Commands

All examples use the ww alias. willow and ww are interchangeable.

<willow-base> defaults to ~/.willow. You can change it with WILLOW_BASE_DIR, the global baseDir config, or ww migrate-base <path>.

Setup

ww clone <url> [name]

Bare-clone a repo and create an initial worktree on the default branch. Required entry point — all willow-managed repos must be set up via ww clone.

ww clone git@github.com:org/repo.git
ww clone git@github.com:org/repo.git myrepo    # custom name
ww clone git@github.com:org/repo.git --force    # re-clone from scratch
FlagDescriptionDefault
--forceRemove existing repo and re-clone from scratchfalse

What happens under the hood:

  1. git clone --bare <url> <willow-base>/repos/<name>.git
  2. Configure remote fetch refs
  3. git fetch origin
  4. Create an initial worktree on the default branch

ww shell-init [flags]

Print shell integration script.

eval "$(willow shell-init)"          # bash / zsh
willow shell-init | source           # fish
eval "$(willow shell-init --tab-title)"  # with tab titles
FlagDescription
--tab-titleInclude terminal tab title hook

Worktrees

ww new [branch] [flags]

Create a new worktree with a new branch, an existing branch, a detached HEAD, or a GitHub PR.

ww new feature/auth                    # create worktree
ww new feature/auth -b develop         # fork from specific branch
ww new -e existing-branch              # use existing branch
ww new -e                              # pick from remote branches (fzf)
ww new --detach                        # detached worktree named detached-<sha>
ww new scratch-repro --detach          # named detached worktree at default base
ww new v1-debug --detach --ref v1.2.3  # named detached worktree at a tag/commit
ww new --pr 123                        # checkout PR #123
ww new https://github.com/org/repo/pull/123  # checkout a PR by URL
ww new feature/auth -r myrepo          # target a specific repo
ww new feature/auth                    # auto-cd via shell integration (tmux-aware)
FlagDescriptionDefault
-b, --baseBase branch to fork fromConfig default / auto-detected
-r, --repoTarget repo by nameAuto-detected from cwd
-e, --existingUse an existing branch (or pick from fzf if no branch given)false
--detachCreate a detached HEAD worktree; name is optionalfalse
--refCommit, tag, or branch to check out in detached modeBase branch
--prGitHub PR number or URL
--no-fetchSkip fetching from remotefalse
--cdPrint only the path (for scripting)false

Detached worktrees

Use --detach when you want a scratch worktree without creating a branch yet. Without a name, Willow creates a generated label like detached-a13f09c; with a name, that name becomes the worktree directory and tmux session identity.

ww new --detach
ww new scratch-repro --detach
ww new old-release --detach --ref v1.2.3
ww promote scratch-repro feature/repro-fix

Existing branch picker

Running ww new -e without a branch name opens an fzf picker showing all remote branches that don't already have worktrees. Select one to create a worktree for it.

From the tmux picker, Ctrl-B creates a stacked worktree from a picked base branch, and Ctrl-E opens the same existing-branch flow from cached refs first, then refreshes remote branches in the background so large repos feel instant to open.

GitHub PR support

Use --pr with a PR number or pass a full PR URL as the branch argument. Willow resolves the branch name via gh, fetches it, and creates the worktree. Requires the GitHub CLI (gh).

ww new --pr 123                              # by number
ww new https://github.com/org/repo/pull/123  # by URL

This also works from the tmux picker — press Ctrl-P for a dedicated PR picker, or paste a PR URL and press Ctrl-N.

ww promote [worktree] <branch>

Promote a detached worktree to a normal branch-backed worktree.

If you run it from inside a detached worktree, pass only the branch name. From another directory, pass the detached worktree name first. With one argument outside an explicitly named detached worktree, Willow uses that name for both the worktree and branch.

Generated detached worktrees like detached-a13f09c require an explicit branch name. When promoted, Willow moves their directory, Claude status dir, and tmux session to the promoted branch identity. Explicitly named detached worktrees keep their existing directory and tmux session name.

ww promote feature/auth                 # from inside a detached worktree
ww promote scratch-repro feature/auth   # promote by worktree name
ww promote scratch-repro                # promote to branch scratch-repro
FlagDescriptionDefault
-r, --repoTarget repo by nameAuto-detected from cwd / all repos
-b, --baseRecord a stack parent for the promoted branch
--cdPrint the final worktree pathfalse

ww rename [worktree] <name> (alias: mv)

Rename a worktree safely. Branch-backed worktrees rename the local branch and move the worktree directory. Detached worktrees only move the worktree directory. Claude status files and matching tmux sessions move with the worktree.

ww rename better-name              # rename the current worktree
ww rename old-name better-name     # rename a selected worktree
ww rename old-name better-name -r myrepo
ww rename old-name better-name --remote

Willow refuses to overwrite existing branches, worktree paths, status directories, or tmux sessions. By default, remote branches are left unchanged; if origin/old-name exists, Willow warns and retargets local upstream config to origin/better-name so the branch no longer tracks origin/old-name.

Use --remote to push the new branch and delete the old remote branch. Before deleting the old remote branch, Willow verifies it has no remote-only commits.

FlagDescriptionDefault
-r, --repoTarget repo by nameAuto-detected from cwd / all repos
--remotePush the new branch and delete the old remote branchfalse

ww checkout <branch-or-pr-url> (alias: co)

Smart switch-or-create. Figures out the right action based on what exists:

  1. Worktree exists for that branch → switch to it (like ww sw)
  2. Branch exists on remote but no worktree → create a worktree for it (like ww new -e)
  3. Branch doesn't exist → create a new branch + worktree (like ww new)
  4. PR URL → resolve the branch via gh, then apply the logic above
ww checkout auth-refactor                # switch if exists, create if not
ww checkout --pr 123                     # checkout PR #123
ww checkout https://github.com/org/repo/pull/123  # checkout a PR by URL
ww checkout brand-new-feature            # creates new branch + worktree
ww checkout brand-new -b develop         # new branch forked from develop
ww checkout auth-refactor                # auto-cd via shell integration (tmux-aware)
FlagDescriptionDefault
-r, --repoTarget repo by nameAuto-detected from cwd
-b, --baseBase branch (only when creating a new branch)Config default / auto-detected
--prGitHub PR number or URL
--no-fetchSkip fetching from remotefalse
--cdPrint only the path (for scripting)false

ww sw

Switch worktrees via fzf. Shows Claude Code agent status per worktree, sorted by urgency: WAIT, unread DONE, BUSY, read DONE, IDLE, then offline.

⏳ WAIT   payments             <willow-base>/worktrees/repo/payments
✅ DONE●  api-cleanup          <willow-base>/worktrees/repo/api-cleanup
🤖 BUSY   auth-refactor        <willow-base>/worktrees/repo/auth-refactor
🟡 IDLE   main                 <willow-base>/worktrees/repo/main
   --     old-feature          <willow-base>/worktrees/repo/old-feature

Pass a name to skip the picker: ww sw auth-refactor.

FlagDescriptionDefault
-r, --repoTarget repo by name (defaults to all repos)Auto-detected from cwd

ww rm [branch] [flags]

Remove a worktree. Without arguments, opens fzf picker with multi-select (TAB to toggle, Ctrl-A to select all).

From the tmux picker, Ctrl-D removes the selected worktree and Ctrl-X bulk-removes safe stale worktrees currently shown in the picker. Stale means the exact current-head PR is merged or the branch's configured upstream is gone; the active tmux session, dirty worktrees, stacked parents, and remote-gone branches with local-only commits are skipped.

ww rm auth-refactor              # direct removal
ww rm                            # fzf picker
ww rm auth-refactor --force      # skip safety checks
ww rm auth-refactor --prune      # also run git worktree prune
FlagDescriptionDefault
-f, --forceSkip safety checksfalse
--keep-branchKeep the local branchfalse
--pruneRun git worktree prune afterfalse

Safety checks (unless --force):

  • Warns if there are uncommitted changes
  • Warns if there are unpushed commits

ww ls [repo]

List worktrees with status.

  BRANCH               STATUS  PATH                                        AGE
  payments             WAIT    <willow-base>/worktrees/myrepo/payments     1d
  auth-refactor        BUSY    <willow-base>/worktrees/myrepo/auth-refactor 2h
  main                 IDLE    <willow-base>/worktrees/myrepo/main         3d
  old-feature          --      <willow-base>/worktrees/myrepo/old-feature  5m

Uses the same urgency ordering as ww sw, while keeping stacked branches together and PR-merged worktrees at the bottom. GitHub-backed merged markers use cached exact PR-state data so ww ls does not refresh GitHub on the hot path.

When run outside a willow repo, lists all repos and their worktree counts.

FlagDescription
--jsonJSON output
--path-onlyPaths only (one per line)

Stacks

Stacked PRs

Create a chain of branches where each builds on the previous:

ww new feature-a -b main              # start a stack
ww new feature-b -b feature-a         # stack on top
ww new feature-c -b feature-b         # third layer

When --base points to a local branch (another worktree), willow forks from it directly and records the parent relationship in branches.json per repo. Stacked branches are shown as a tree in ww ls and the tmux picker:

  BRANCH                    STATUS  PATH                                           AGE
  ├─ feature-a              BUSY    <willow-base>/worktrees/repo/feature-a         2h
  │  └─ feature-b           DONE    <willow-base>/worktrees/repo/feature-b         1h
  │     └─ feature-c        --      <willow-base>/worktrees/repo/feature-c         30m
  standalone                --      <willow-base>/worktrees/repo/standalone        1d

Removing a stacked branch re-parents its children to the removed branch's parent. Use --force if the branch has children.

ww stack status (alias: ww stack s)

Show CI, review, and merge status for every PR in a stack at a glance. Fetches all data in a single gh pr list call, so it's O(1) regardless of stack depth.

ww stack status                    # current repo
ww stack status -r myrepo          # target a specific repo
ww stack status --json             # machine-readable output
  feature-a              #42  ✓ CI  ✓ Review  MERGEABLE  +100 -20
  └─ feature-b           #43  ✗ CI  ◯ Review  CONFLICTING  +50 -10
     └─ feature-c        (no PR)

Each branch shows:

  • PR number — links to the GitHub PR
  • CI status — aggregate of all checks: ✓ CI (pass), ✗ CI (fail), ◯ CI (pending), - CI (none)
  • Review status✓ Review (approved), ✗ Review (changes requested), ◯ Review (required), - Review (none)
  • MergeableMERGEABLE, CONFLICTING, or UNKNOWN
  • Diff stats — additions and deletions
FlagDescriptionDefault
-r, --repoTarget repo by nameAuto-detected from cwd
--jsonOutput as JSONfalse

Requires the GitHub CLI (gh).

ww sync [branch]

Rebase stacked worktrees onto their parents in topological order. Like git machete traverse but for worktrees.

ww sync                    # sync all stacks in current repo
ww sync feature-b          # sync only feature-b and its descendants
ww sync -r myrepo          # target specific repo
ww sync --abort            # abort any in-progress rebases
FlagDescriptionDefault
-r, --repoTarget repo by nameAuto-detected from cwd
--no-fetchSkip git fetch originfalse
--abortAbort in-progress rebases across all stacked worktreesfalse

How it works:

  1. Fetches origin once
  2. Processes branches in topological order (parents before children)
  3. For root branches (parent is main): rebases onto origin/main
  4. For stacked branches: rebases onto the local parent (which was just synced)
  5. On conflict: stops descendants of the conflicting branch, continues other stacks

Dirty worktrees and in-progress rebases are skipped with a warning.

ww pr create

Create a GitHub PR for the current worktree. Willow derives the PR base from the stack parent when the branch is stacked, or from the repo's default base branch otherwise. It pushes the branch if needed, skips creation if an open PR already exists, and can publish the current branch's ancestor stack in root-to-current order.

ww pr create                  # create PR for the current branch
ww pr create --draft          # create a draft PR
ww pr create --stack          # create missing PRs for root → current branch
FlagDescriptionDefault
--draftCreate draft pull requestsfalse
--stackCreate missing PRs for the current branch's ancestor stackfalse

Behavior:

  1. Must be run from inside a willow-managed worktree
  2. Refuses to run if the current worktree has uncommitted changes
  3. Uses the stack parent as the PR base when the branch is stacked
  4. Pushes the branch if the remote is missing or behind
  5. Reuses an existing open PR instead of creating a duplicate

Requires the GitHub CLI (gh).

Inspection

ww status

Rich view of Claude Code agent status across all worktrees. Shows per-session rows when multiple Claude sessions run in the same worktree, with a short session ID on each session row.

myrepo (4 worktrees, 3 sessions active, 1 unread)

  🤖 auth-refactor [4f2c9a1b]  BUSY    2m ago
  🤖 auth-refactor [9b7d2e44]  BUSY    5m ago
  ✅ payments      [3ac18f02]  DONE●   12m ago
  🟡 main                     IDLE    1h ago
     old-feature              --

The indicator marks completed sessions you haven't reviewed yet. Switching to a worktree via ww sw marks it as read.

FlagDescription
--jsonJSON output

ww dashboard (alias: dash, d)

Live-refreshing TUI showing every worktree across all repos. Renders in an alternate screen buffer with no flicker. Uses the same stacked-branch tree grouping as the tmux picker, with aggregate agent status, unread counts, merged markers, and worktree paths.

ww dashboard              # default 2s refresh
ww dash -i 5              # 5s refresh interval

ww dashboard

FlagDescription
-i, --intervalRefresh interval in seconds (default: 2)

Press Ctrl-C to quit.

ww log

Show activity log of worktree events. Events are recorded automatically when you create, remove, or sync worktrees. Stored as monthly JSONL files under <willow-base>/log/.

ww log                          # last 20 events
ww log --branch auth-refactor   # filter by branch
ww log --repo myrepo            # filter by repo
ww log --since 7d               # events from last 7 days
ww log -n 50                    # last 50 events
ww log --json                   # raw JSON output
FlagDescription
--branchFilter by branch name
-r, --repoFilter by repo name
--sinceShow events after duration (e.g. 7d, 24h, 30m)
-n, --limitMax events to show (default 20)
--jsonJSON output

Event types:

ActionTrigger
createWorktree created via ww new or ww checkout
renameWorktree renamed via ww rename
removeWorktree removed via ww rm
syncBranch rebased via ww sync

ww gc

Clean up leftover trash from removed worktrees and list stale worktrees. Stale candidates are worktrees whose exact current-head PR is merged or whose configured upstream branch is gone after git fetch --prune.

ww gc                    # empty <willow-base>/trash/ and list stale worktrees
ww gc --repo myrepo      # scan one repo
ww gc --no-fetch         # use local remote-tracking refs as-is
ww gc --prune            # confirm and remove the safe subset
ww gc --dry-run          # preview without touching anything
ww gc --prune --dry-run  # list what --prune would remove
FlagDescriptionDefault
--pruneInteractively remove safe stale worktreesfalse
--dry-runShow what would be cleaned up without removing anythingfalse
-r, --repoTarget a willow-managed repo by nameAll repos
--no-fetchSkip git fetch --prune before scanningfalse

Without --prune, willow only empties the trash directory and prints the commands you'd run to remove each stale worktree. With --prune, it removes only safe stale candidates: dirty worktrees, branches with stacked children, and remote-gone branches whose commits are not reachable from their expected base are skipped. PR-merged worktrees use the existing exact GitHub match and do not fall back to Git ancestry.

Agents

Desktop notifications

Desktop notifications fire directly from Claude Code's hook system after you run ww cc-setup — there's no daemon, no polling, and no ww notify subcommand. Whenever an agent transitions from BUSY to DONE or WAIT, the hook fires a macOS Notification Center alert within ~200ms.

Desktop notifications: Enabled by default. Set "notify": {"desktop": false} in config to disable them. The tmux status bar is a separate channel and is unaffected.

Custom notification command: Set notify.command in config to run your own script instead of the built-in dispatch. The command receives WILLOW_NOTIFY_TITLE and WILLOW_NOTIFY_BODY as environment variables.

Concurrency: When multiple sessions run in the same worktree, an advisory flock on <willow-base>/notify-states.lock prevents duplicate notifications.

ww dispatch <prompt>

Create a worktree and launch Claude Code with a prompt. From the terminal, Claude runs interactively in the foreground. From the tmux picker (Ctrl-G), it launches in a background session.

ww dispatch "Fix the login validation bug"                  # auto-name branch
ww dispatch "Add retry logic" --name add-retries             # explicit branch name
ww dispatch "Write tests for auth" --base feature/auth       # stacked on a branch
ww dispatch "Refactor payments" --repo myrepo                # target specific repo
FlagDescriptionDefault
--nameWorktree/branch nameAuto-slugified from prompt (e.g. dispatch--fix-the-login-validation)
-r, --repoTarget repo by nameAuto-detected from cwd
-b, --baseBase branch to fork fromConfig default
--no-fetchSkip fetching from remotefalse
--yoloRun Claude with --dangerously-skip-permissionsfalse

How it works:

  1. Creates a new worktree (reuses ww new internally)
  2. Launches claude "<prompt>" (interactive session with prompt pre-loaded)
  3. Logs a dispatch event (visible in ww log)

The agent appears in ww status, ww dashboard, and the tmux picker. Switch to it anytime with ww sw.

ww cc-setup

Hook installation for Claude Code status tracking. Safe to re-run: each invocation overwrites willow's entries in ~/.claude/settings.json with the current binary path, so upgrades and relocations stay in sync. Third-party hook rules are left untouched.

  1. Creates the status directory: <willow-base>/status/
  2. Registers the hidden willow hook subcommand for every event willow tracks. The absolute binary path is resolved via os.Executable() at install time.

The hook fires on 6 events (UserPromptSubmit, PreToolUse, PostToolUse, Stop, Notification, SessionEnd). Each invocation writes per-session status to <willow-base>/status/<repo>/<worktree>/<session_id>.json, appends to the session timeline, and dispatches desktop notifications on status transitions. Multiple Claude sessions in the same worktree are tracked independently. The SessionEnd event immediately removes the session file for instant cleanup.

The hook also tracks enriched session data:

  • tool_count — number of tool invocations in the session
  • start_time — when the session first became active
  • files_touched — files written/edited by the agent (stored in a .files sidecar)

Agent status

After running ww cc-setup, Claude Code automatically reports its state:

IconStatusMeaning
🤖BUSYAgent is actively working
DONEAgent finished its turn
WAITAgent is waiting for user input
🟡IDLEAgent session ended
--No activity detected

Stale BUSY/WAIT status (>2 min) automatically degrades to IDLE. Completed sessions stay DONE until the session ends. Completed sessions show a unread indicator until you switch to that worktree via ww sw.

Configuration

ww config

View, edit, and initialize willow configuration. Merges global (~/.config/willow/config.json) and local (per-repo willow.json) configs, showing the effective value and source for each field. baseDir is global-only and resolved before repo-local config is loaded.

ww config show               # show merged config with source annotations
ww config show --json        # raw JSON output
ww config show -r myrepo     # show config for a specific repo
ww config edit               # open global config in $EDITOR
ww config edit --local       # open local (per-repo) config
ww config edit --local -r myrepo  # open specific repo's local config
ww config init               # create default global config
ww config init --local       # create default local config
ww config init --force       # overwrite existing config

ww config show

FlagDescriptionDefault
--jsonOutput raw JSONfalse
-r, --repoTarget repo by nameAuto-detected from cwd

ww config edit

FlagDescriptionDefault
--globalEdit global config (default)true
--localEdit local (per-repo) configfalse
-r, --repoTarget repo by nameAuto-detected from cwd

Editor resolution: $VISUAL > $EDITOR > vi.

ww config init

FlagDescriptionDefault
--localCreate local (per-repo) configfalse
-r, --repoTarget repo by nameAuto-detected from cwd
--forceOverwrite existing configfalse

ww migrate-base <path>

Move willow's base directory to a new path, repair Git worktree metadata, and persist the new global baseDir.

ww migrate-base ~/code/worktrees/willow
ww migrate-base ~/code/worktrees/willow --dry-run
ww migrate-base ~/code/worktrees/willow --yes
FlagDescriptionDefault
--dry-runShow the planned move without changing anythingfalse
-y, --yesSkip confirmationfalse

The command rejects overlapping paths, non-empty destinations, active Claude session files, and running from inside the current willow base.

ww doctor

Check your willow setup for common issues. Runs a series of checks and prints a checklist:

ww doctor          # report only
ww doctor --fix    # prompt to remove unmarked legacy willow hooks from settings.json
✔ git 2.45.0
✔ gh CLI installed
✔ tmux installed
✔ Claude Code hooks installed
✔ <willow-base> exists
✔ <willow-base>/repos exists
✔ <willow-base>/worktrees exists
✔ no stale session files
✔ config valid
CheckDetails
git versionWarns if < 2.30
gh CLIOptional — warns if missing
tmuxOptional — warns if missing
Claude Code hooksChecks ~/.claude/settings.json for willow hooks; flags unmarked legacy entries from older releases
Willow directories<willow-base>, repos/, worktrees/
Stale sessionsSession files older than 30 minutes
Config validityValidates merged config for common issues

--fix flag: When legacy willow hooks are detected, ww doctor --fix prompts for confirmation and removes them from ~/.claude/settings.json. Marker-tagged (source: "willow") and third-party rules are never touched.

Reference

ww tmux

Tmux integration for worktree management. See the Tmux Integration page for full documentation.

ww tmux pick              # interactive worktree picker (for tmux popup)
ww tmux list              # formatted picker lines (for diagnostics)
ww tmux status-bar        # tmux status-right widget
ww tmux install           # print tmux.conf lines to add

Setup: ww tmux install prints the config to add to ~/.tmux.conf, including a prefix + w keybinding for the picker popup. Inside the picker, Ctrl-T creates a detached worktree from the selected HEAD, Ctrl-U promotes a selected detached worktree, Ctrl-D deletes the selected worktree, and Ctrl-X bulk-deletes safe stale worktrees currently shown, skipping the active tmux session plus any dirty worktrees, stacked parents, or remote-gone branches with local-only commits. By default, the picker shows a right-side live preview. Set "tmux": {"switcherPreview": false} in config to hide it, use the full popup for fzf, and make ww tmux install emit a compact 70% by 70% popup binding.

Aliases

AliasCommand
ww nww new
ww mvww rename
ww coww checkout
ww lww ls
ww sww status
ww dash / ww dww dashboard
ww stack sww stack status

Global flags

-C <path>       Run as if willow was started in <path> (env: WILLOW_DIR)
--verbose       Show git commands being executed
--trace         Print timing trace to stderr for performance debugging (env: WILLOW_TRACE)

skills/willow-autoresearch/scripts/bench_willow.py consumes --trace output while running with WILLOW_TELEMETRY=off, so performance measurements stay focused on the local CLI path.