Use the README for the product overview and demo. This page keeps the full install, usage, command, and release reference.
For the best interactive workflow, install fzf. If fzf is not available, ww falls back to the built-in arrow-key selector automatically.
Homebrew installs the helper and shell library, but leaves shell activation to you.
brew tap unix2dos/ww https://github.com/unix2dos/ww
brew install wwFor Zsh:
printf 'eval "$("%s/bin/ww-helper" init zsh)"\n' "$(brew --prefix ww)" >> ~/.zshrc
source ~/.zshrcFor Bash:
printf 'eval "$("%s/bin/ww-helper" init bash)"\n' "$(brew --prefix ww)" >> ~/.bashrc
source ~/.bashrcww-helper init zsh and ww-helper init bash print the activation snippet if you want to inspect it before adding it to your shell rc file.
Install the latest release for your current platform:
curl -fsSL https://github.com/unix2dos/ww/releases/latest/download/install-release.sh | bash
source ~/.zshrcFor Bash:
curl -fsSL https://github.com/unix2dos/ww/releases/latest/download/install-release.sh | bash -s -- --shell bash --rc-file ~/.bashrc
source ~/.bashrcInstall a specific version:
curl -fsSL https://github.com/unix2dos/ww/releases/latest/download/install-release.sh | WT_VERSION=vX.Y.Z bashThis path does not require Go. It downloads the installer script from the latest GitHub Release, then fetches the matching release archive for your platform and runs the bundled installer.
git clone https://github.com/unix2dos/ww.git
cd ww
bash install.sh
source ~/.zshrcIf you use Bash, reload with source ~/.bashrc instead.
The installer puts ww-helper and ww.sh into your target bin directory, then appends a managed shell block that exposes ww.
Source installs require a working Go toolchain.
tar -xzf ww-vX.Y.Z-darwin-arm64.tar.gz
cd ww-vX.Y.Z-darwin-arm64
bash install.sh
source ~/.zshrcRelease bundle installs copy the prebuilt bin/ww-helper binary and ww.sh, and do not require Go.
bash install.sh --shell zsh
bash install.sh --shell bash --rc-file ~/.bashrc
bash install.sh --bin-dir ~/.local/binbash uninstall.sh
source ~/.zshrcIf you installed into Bash, reload ~/.bashrc instead.
ww only works for the current repository. Run it inside a Git repository or one of that repository's worktrees.
ww is a shell function that switches worktrees and changes your current shell directory.
wworww switchselects a worktree and switches into it.ww listprints worktrees without changing directory.ww list --verboseadds labels, intent, and metadata.ww new <name>creates a new worktree under./.worktrees/<name>and switches into it.ww rm [<name>]removes a worktree and deletes its branch only when that branch is already merged into the effective base branch. Without a target,ww rmopens an interactive selector for review-and-remove.ww version(orww --version) prints the binary and protocol version. Local dev installs include the embedded Git commit when available.ww helporww --helpprints the command summary.wwusesfzfautomatically when available and falls back to the built-in arrow-key selector otherwise.
Two integration paths — pick whichever fits the agent. Both are backed by the same v1.1 wire protocol; see protocol.md for the formal contract.
Over MCP (Claude Code, Cursor, Zed, Continue, Cline, Codex, …):
{"mcpServers": {"ww": {"command": "ww-helper", "args": ["mcp", "serve"]}}}Six tools become available: ww_list, ww_new, ww_remove, ww_gc, ww_switch_path, ww_version.
As a subprocess:
ww-helper version --json
ww-helper list --json
ww-helper new-path --json --label agent:claude-code --ttl 24h -m "Fix login redirect" feat-a
ww-helper gc --ttl-expired --idle 7d --dry-run --json
ww-helper rm --json feat-aThe shared integration contract is AGENTS.md plus the machine-readable ww-helper commands. When ww-helper covers a workflow, agents should use it instead of scripting raw git worktree commands.
ww-helper switch-path is a path-printing helper for shell-eval (cd "$(ww-helper switch-path X)") and is intentionally out of the JSON envelope contract; over MCP, the equivalent ww_switch_path tool wraps the path normally.
Successful --json responses:
{
"protocol": "1.1",
"ok": true,
"command": "list",
"data": { ... },
"warnings": []
}Error responses:
{
"protocol": "1.1",
"ok": false,
"command": "rm",
"error": {
"code": "worktree.dirty",
"message": "worktree has uncommitted changes; rerun with --force",
"context": {}
}
}The envelope no longer carries exit_code — the process exit code is the single source of truth. Error codes follow domain.subcode (worktree.dirty, git.repo_missing, selector.fzf_not_installed, input.missing_selector, …); see protocol.md §5 for the full table.
Returns an array of worktrees. Each entry has:
path— absolute filesystem pathbranch— branch labeldirty— boolean; any uncommitted changesactive— boolean; this is the caller's current worktreecreated_at— unix milliseconds;0if unknownlast_used_at— unix milliseconds;0if neverlabel— free-form metadata string;""if nonettl— duration string ("24h","7d");""if nonemerged— branch is merged into the display status base refahead/behind— commits ahead/behind the display status base refstatus_base_ref— ref used formerged,ahead, andbehind; usuallyorigin/HEAD, thenorigin/<default>, then the local default branch when remote refs are unavailablestaged/unstaged/untracked— change counts
Status display never runs git fetch; remote refs are local cached tracking refs. Cleanup and removal safety still use their own conservative base checks.
Returns:
worktree_pathbranch
label is stored as a single free-text string. ttl is fixed from creation time; this release does not include a metadata editing command. -m sets a one-line intent describing what this worktree is for; it appears in ww list --verbose and ww rm safety output.
When label is present, ww-helper also stores extra workspace context for later human summaries. That context is kept in Git's per-worktree admin area, not in tracked files.
gc requires at least one explicit selector. Supported selectors are:
--ttl-expired--idle <duration>--merged
A bare ww-helper gc --json (no selector) returns input.missing_selector with exit code 2.
Dry-run responses use the same envelope and return:
summary.matchedsummary.removedsummary.skippeditems[].matched_rulesitems[].actionitems[].reasonwhen skipped
The JSON path never prompts. Safety rules:
- dirty worktrees require
--force - the active worktree cannot be removed (returns
worktree.remove_current) - if you omit
<target>and more than one removable worktree exists, the command returnsworktree.ambiguous_match
new-path --json automatically syncs git-ignored files (.env and similar) from the main worktree by default; results are surfaced through the envelope's warnings array (sync.copied, sync.skipped, …). Pass --no-sync to opt out, or --sync-dry-run to preview without writing files.
wwWithout fzf, this opens the built-in selector like:
* [1] [CURRENT] main /path/to/repo
[2] feat-a /path/to/repo/.worktrees/feat-a
Use Up/Down (or j/k). Enter to confirm. Esc/Ctrl-C to cancel.
Move with arrow keys and press Enter to switch. The selector starts on the active shell worktree by default.
The status column can show:
[CURRENT]for the current clean worktree[CURRENT] [DIRTY]for the current dirty worktree[DIRTY]for a non-current dirty worktree
ww ignores its own .worktrees/ management directory when computing this status so the main worktree is not marked dirty just because linked worktrees exist.
ww 2
ww switch feat-a
ww switch feaExact name matches win. If no exact match exists, ww falls back to a unique prefix match.
ww list
ww list --verboseThis prints the current worktree table without changing your shell directory.
The human-readable ww list output uses a full Unicode box table. Interactive ww selection stays header-free so the picker remains compact.
Example:
┌───────┬───────────────────┬────────┬──────────────────────────────────────────────────┐
│ INDEX │ STATUS │ BRANCH │ PATH │
├───────┼───────────────────┼────────┼──────────────────────────────────────────────────┤
│ 1 │ [CURRENT] │ main │ /path/to/repo │
├───────┼───────────────────┼────────┼──────────────────────────────────────────────────┤
│ 2 │ [DIRTY] │ feat-a │ /path/to/repo/.worktrees/very/long/path/that/ │
│ │ │ │ wraps/inside/the/path/cell │
└───────┴───────────────────┴────────┴──────────────────────────────────────────────────┘
Worktrees are shown from oldest to newest by worktree creation time. Smaller indices refer to older worktrees, and the status column uses the same [CURRENT] / [CURRENT] [DIRTY] / [DIRTY] tags as the interactive selector.
Long PATH values are wrapped inside the PATH cell instead of being truncated.
Detached worktrees use plain human labels in ww list: clean worktrees with no commits of their own show branch scratch with detail idle; dirty scratch worktrees show local changes; detached worktrees with commits not reachable from the base branch show branch unbranched, the number of commits, and their last commit subject. Idle scratch worktrees do not show a last commit because it is usually just the base commit, not task context.
--verbose appends extra metadata such as stored workspace context and timestamps to the human-readable output.
ww new feat-aThis creates branch feat-a from the current HEAD in ./.worktrees/feat-a, copies git-ignored config files from the main worktree into the new one, then switches into it.
For metadata-aware creation, use ww-helper new-path --json --label ... --ttl ... -m "intent". The -m flag sets a one-line intent that appears in ww list --verbose and ww rm safety output.
When a new worktree is created, ww new automatically copies git-ignored files from the main worktree — typically .env, local config files, and development certificates — so the new workspace is immediately usable.
Flags:
ww new feat-a # default: sync enabled
ww new feat-a --no-sync # skip sync for this run
ww new feat-a --sync-dry-run # preview what would be copied without writing filesWhat gets skipped:
Large dependency and build directories are excluded by default:
- JS/TS:
node_modules/,.next/,.nuxt/,dist/,build/,.vite/,.turbo/,coverage/ - Python:
__pycache__/,.venv/,venv/,env/,.pytest_cache/ - Go/Rust/Java:
vendor/,target/,.gradle/ - General:
tmp/,temp/,logs/,.cache/,.DS_Store
Any file at or above 1 MiB is also skipped as a safety net.
Configuration (~/.config/ww/config.json):
{
"version": 1,
"sync": {
"enabled": true,
"max_file_size": 1048576,
"blacklist_extra": ["my-secrets/", "local-certs/"],
"blacklist_override": null
}
}enabled: set tofalseto disable sync globally.max_file_size: per-file size cap in bytes (default 1 MiB).blacklist_extra: additional path segments appended to the built-in blacklist.blacklist_override: non-null value replaces the built-in blacklist entirely; an empty array[]disables the blacklist completely.
The config file is optional. A missing file uses all built-in defaults. XDG_CONFIG_HOME is honoured; the default path is ~/.config/ww/config.json.
ww rm
ww rm feat-a
ww rm --force feat-a
ww rm --cleanupww rm (no target) opens an interactive selector for review-and-remove. The selector marks clean merged worktrees as safe and marks dirty, unmerged, or branchless worktrees as review with a short reason. With a target, it removes that worktree directly after confirmation. The branch is deleted only when it is already merged into the effective base branch. Dirty worktrees stop before confirmation unless you explicitly rerun with --force.
ww rm --cleanup removes all clearly safe worktrees after one confirmation. Safe means clean files, already merged, and not the base branch. The prompt shows numbered names plus each last commit subject, and keeps base-branch, detached, dirty, or unmerged worktrees out of the deletion list.
cd /path/to/repo
ww
ww switch feat-a
ww list
ww new feat-b
ww rm feat-a
ww rm --cleanup # delete all clearly safe worktrees after one confirmation
ww rm # interactive picker for review-and-removeww, ww 2, and ww switch feat-a all switch the current shell into the target worktree.
0: success2: invalid user input such as a bad index, bad name match, or extra args3: environment problem such as not being in a Git repo130: interactive selection canceled
ww-helper ... --json envelopes do not carry exit_code; rely on the process exit code.
ww --help
ww help
ww --version
ww 1
ww switch feat-a
ww list
ww new feat-b
ww rm feat-a
ww rmInstaller checks:
bash install.sh
bash install.shBuild release archives locally:
bash scripts/release.sh vX.Y.ZArtifacts are written to dist/:
ww-vX.Y.Z-darwin-arm64.tar.gzww-vX.Y.Z-darwin-amd64.tar.gzww-vX.Y.Z-linux-arm64.tar.gzww-vX.Y.Z-linux-amd64.tar.gzchecksums.txtinstall-release.shww.rb
Refresh the committed Homebrew formula after a release is published:
bash scripts/generate-homebrew-formula.sh vX.Y.Z Formula/ww.rb
git add Formula/ww.rb
git commit -m "chore: update Homebrew formula for vX.Y.Z"
git push origin mainTo publish a GitHub Release, create and push a tag matching v*:
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git push origin vX.Y.ZGitHub release publishing is wired through .github/workflows/release.yml and only publishes when the workflow runs for refs/tags/v*.
Manual workflow_dispatch runs still build the dist/ artifacts, including ww.rb, but they do not publish a GitHub Release.