Skip to content
iamrajjoshiPublic

About

a worktree manager with agent status tracking

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

174 Commits

Folders and files

Repository files navigation

willow logo

willow

willow

A git worktree manager built for AI agent workflows.

Docs GitHub release CI License

Spin up isolated worktrees for Claude Code, Codex CLI, or Cursor Agent sessions.
Switch between them instantly with fzf.
See which agents are busy, waiting, or idle.

Documentation


demo

Why willow?

Running multiple agent sessions on the same repo means constant context-switching, stashing, and branch juggling. Willow fixes this by giving every task its own isolated directory via git worktrees, then adding fzf-based switching and live agent status tracking on top.

<willow-base>/
β”œβ”€β”€ repos/
β”‚   └── myrepo.git/              # bare clone (shared git database)
β”œβ”€β”€ worktrees/
β”‚   └── myrepo/
β”‚       β”œβ”€β”€ main/                 # each branch = isolated directory
β”‚       β”œβ”€β”€ auth-refactor/        # Claude Code, Codex, or Cursor running here
β”‚       └── payments/             # another agent running here
└── status/
    └── myrepo/
        β”œβ”€β”€ auth-refactor/
        β”‚   └── claude/
        β”‚       └── <session_id>.json   # {"status": "BUSY", ...}
        └── payments/
            β”œβ”€β”€ codex/
            β”‚   └── <session_id>.json   # {"status": "DONE", ...}
            └── cursor/
                └── <session_id>.json   # {"status": "BUSY", ...}

<willow-base> defaults to ~/.willow. You can override it with WILLOW_BASE_DIR or the global baseDir config, and move an existing setup with ww migrate-base <path>.

Install

Homebrew

brew install iamrajjoshi/tap/willow

From source

go install github.com/iamrajjoshi/willow/cmd/willow@latest

Requirements

  • git
  • tmux β€” optional, for the ww tmux picker popup
  • gh β€” optional, required for ww new --pr, ww stack status, ww pr create, and PR-state merged worktree detection
  • terminal-notifier β€” optional on macOS, for clickable agent notifications (brew install terminal-notifier)

Setup

1. Shell integration

# Add to .bashrc / .zshrc
eval "$(willow shell-init)"

# fish
willow shell-init | source

This gives you:

Command Description
ww <cmd> Alias for willow
ww sw fzf worktree switcher (cd's into selection)
ww new <branch> Create worktree + cd into it (tmux-aware)
ww new [name] --detach Create a detached worktree without a branch
ww promote [name] <branch> Promote a detached worktree to a branch
ww rename [worktree] <name> Rename a worktree, branch, status dir, and tmux session
ww checkout <branch> Smart checkout + cd (switch or create, tmux-aware)
ww gc Clean trash and list stale worktrees
wwn <branch> Shorthand for ww new
wwc <branch> Shorthand for ww checkout
www cd to <willow-base>/worktrees/

Optional: Set the terminal tab title to the current worktree name. This also lets notification clicks select the matching Ghostty 1.3+, iTerm2, or Terminal.app tab when the agent isn't running in tmux:

eval "$(willow shell-init --tab-title)"

2. Claude Code skill (optional)

Teach Claude Code how to use willow automatically. The repo ships a skill at skills/willow/SKILL.md that the skills CLI can install:

# Global (available across all projects)
npx skills add iamrajjoshi/willow --skill willow -g -a claude-code

# Project-local (only inside the current project)
npx skills add iamrajjoshi/willow --skill willow -a claude-code

# Or clone directly
git clone https://github.com/iamrajjoshi/willow ~/.claude/skills/willow

-g installs to ~/.claude/skills/, -a claude-code targets Claude Code specifically, and --skill willow avoids installing everything in the repo. Once installed, Claude Code will reach for ww checkout, ww sync, ww dispatch, and other willow commands automatically when you ask it to work on branches, PRs, or parallel tasks.

3. Autoresearch performance skill (optional)

Willow also ships skills/willow-autoresearch/, a trace-guided benchmark loop for making the CLI faster without Sentry in the measurement path. The harness builds Willow, creates an isolated fixture, runs commands with WILLOW_TELEMETRY=off and WILLOW_TRACE=1, and writes local results under .autoresearch/.

python3 skills/willow-autoresearch/scripts/bench_willow.py --runs 7 --json
python3 skills/willow-autoresearch/scripts/bench_willow.py --reference-repo ~/code/myrepo --runs 7 --json

--reference-repo accepts a local repo path, an existing Willow repo name, or a git URL. The source repo is only used as a read-only fixture template.

4. Agent status tracking (optional)

ww cc-setup                 # Claude Code hooks
ww codex-setup              # Codex CLI hooks
ww cursor-setup             # Cursor Agent hooks
ww agent setup all          # all built-in harnesses

Installs hooks that write per-session agent status (BUSY / DONE / WAIT / IDLE) to <willow-base>/status/<repo>/<worktree>/<harness>/. Claude hooks live in ~/.claude/settings.json; Codex hooks live in ~/.codex/hooks.json and may need review in Codex with /hooks; Cursor hooks live in ~/.cursor/hooks.json. Cursor currently reports BUSY, DONE, and cleanup events, but not WAIT. Multiple harnesses can run in the same worktree at the same time. This powers the status column in ww ls, ww sw, ww status, and ww dashboard.

Quick start

# Clone a repo (one-time)
ww clone git@github.com:org/myrepo.git

# Create a worktree and cd into it
ww new auth-refactor

# Start Claude Code, Codex, or Cursor Agent
claude
# or: codex
# or: cursor-agent

# In another terminal β€” create a second worktree
ww new payments-fix
claude

# Switch between worktrees (fzf picker with agent status)
ww sw

# Check on all agents
ww status

# Clean up when done
ww rm auth-refactor

Commands

ww clone <url> [name]

Bare-clone a repo and create an initial worktree on the default branch. Required entry point.

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

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)

From the tmux picker, Ctrl-T creates a detached worktree from the selected HEAD, Ctrl-U promotes a selected detached worktree to a branch, Ctrl-B creates a stacked worktree from a picked base branch, and Ctrl-E opens the existing-branch flow from cached refs first, then refreshes remote branches in the background so opening the picker does not wait on a remote fetch.

Flag Description
-b, --base Base branch to fork from
-r, --repo Target repo by name
-e, --existing Use an existing branch (or pick from fzf if no branch given)
--detach Create a detached HEAD worktree; name is optional
--ref Commit, tag, or branch to check out in detached mode
--pr GitHub PR number or URL
--no-fetch Skip fetching from remote
--cd Print only the path (for scripting)

ww promote [worktree] <branch>

Promote a detached worktree to a normal branch-backed worktree. Nameless detached worktrees use generated labels like detached-a13f09c; when promoted, Willow moves their directory, agent status dir, and tmux session to the promoted branch identity. Explicitly named detached worktrees keep their directory and tmux session name unless you rename them.

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
Flag Description
-r, --repo Target repo by name
-b, --base Record a stack parent for the promoted branch
--cd Print the final path (for scripting)

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

Rename a worktree safely. For branch-backed worktrees, Willow renames the local branch and moves the worktree directory. For detached worktrees, Willow only renames the worktree directory. Agent 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 after Willow verifies the old remote branch has no remote-only commits.

Flag Description
-r, --repo Target repo by name
--remote Push the new branch and delete the old remote branch

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

Switch to an existing worktree, create one for a remote branch, or create a new branch. Worktrees whose exact current-head PR is merged show a [merged] indicator in ww ls and the tmux picker. The tmux picker uses cached GitHub PR-state results on open so slow PR lookups don't block rendering.

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 from develop
ww checkout auth-refactor                # auto-cd via shell integration (tmux-aware)
Flag Description
-r, --repo Target repo by name
-b, --base Base branch (only when creating a new branch)
--pr GitHub PR number or URL
--no-fetch Skip fetching from remote
--cd Print only the path (for scripting)

Stacked PRs

Create stacked branches with --base:

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

Stacked branches are shown as a tree in ww ls and the tmux picker. Parent relationships are tracked in branches.json per repo.

ww ls with stacks

ww stack status (alias: ww stack s)

Show CI, review, and merge status for every PR in a stack at a glance. Fetches all PR data in a single gh pr list call.

ww stack status                    # current repo
ww stack status -r myrepo          # target a specific repo
ww stack status --json             # JSON output
  feature-a              #42  βœ“ CI  βœ“ Review  MERGEABLE  +100 -20
  └─ feature-b           #43  βœ— CI  β—― Review  CONFLICTING  +50 -10
     └─ feature-c        (no PR)
Flag Description
-r, --repo Target repo by name
--json JSON output

Requires the GitHub CLI (gh).

ww sync [branch]

Rebase stacked worktrees onto their parents in topological order.

ww sync                    # sync all stacks in current repo
ww sync feature-b          # sync feature-b and its descendants only
ww sync --abort            # abort any in-progress rebases
Flag Description
-r, --repo Target repo by name
--no-fetch Skip fetching from remote
--abort Abort in-progress rebases

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 order.

ww pr create                  # create PR for the current branch
ww pr create --draft          # open a draft PR
ww pr create --stack          # create missing PRs from root β†’ current branch
Flag Description
--draft Create draft pull requests
--stack Create missing PRs for the current branch's ancestor stack

Requires the GitHub CLI (gh) and must be run from inside a willow-managed worktree with a clean working tree.

ww sw

Switch worktrees via fzf. Shows 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

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
Flag Description
-f, --force Skip safety checks
--keep-branch Keep the local branch
--prune Run git worktree prune after

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 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 --prune --dry-run  # show what --prune would remove
Flag Description
--prune Interactively remove safe stale worktrees
--dry-run Show what would be cleaned up without removing anything
-r, --repo Target a willow-managed repo by name
--no-fetch Skip git fetch --prune before scanning

ww gc --prune skips dirty worktrees, branches with stacked children, and remote-gone branches whose commits are not reachable from their expected base. PR-merged worktrees use the existing exact GitHub match and do not fall back to Git ancestry.

ww ls [repo]

List worktrees with status. 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 stays fast in large repos.

ww ls

Flag Description
--json JSON output
--path-only Paths only (one per line)

ww status

Show agent status per worktree/session. Multiple sessions in the same worktree render child rows with labels like [claude] a044b2af and unread indicators (●) for completed sessions you haven't reviewed.

ww status

Flag Description
--json JSON output

ww dashboard (alias: dash, d)

Live-refreshing TUI showing every worktree across all repos. 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

Press Ctrl-C to quit.

ww dashboard

ww log

Show activity log of worktree events (creates, renames, removes, syncs).

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
Flag Description
--branch Filter by branch name
-r, --repo Filter by repo name
--since Show events after duration (e.g. 7d, 24h)
-n, --limit Max events to show (default 20)
--json JSON output

Desktop notifications

Desktop notifications fire directly from agent hook systems β€” no daemon, no polling. Run ww agent setup all once; whenever an agent transitions from BUSY to DONE or WAIT, a macOS Notification Center alert appears within ~200ms.

Desktop notifications are enabled by default. Set "notify": {"desktop": false} to disable them, or set "notify": {"command": "..."} to run a custom shell command instead (it receives WILLOW_NOTIFY_TITLE and WILLOW_NOTIFY_BODY env vars). The tmux status bar widget uses a separate sound-only channel and is unaffected.

On macOS, clicking a built-in notification can return you to the agent that triggered it. Install terminal-notifier with brew install terminal-notifier; Willow then switches the captured tmux session and brings its terminal forward in Ghostty 1.3+, iTerm2, or Terminal.app. Without terminal-notifier, Willow falls back to a plain osascript notification with no click action. Run ww doctor to check whether it is installed.

For agents outside tmux, supported terminals can select the matching tab when you enable tab titles with eval "$(willow shell-init --tab-title)". Without tab titles, or in another terminal, a click only brings the terminal app forward. Ghostty users must also disable Ghostty's own title updates as described in the Terminal setup section below.

ww dispatch <prompt> [flags]

Create a worktree and launch the configured agent harness with a prompt. From the terminal, the agent runs interactively in the foreground. From the tmux picker, Ctrl-G launches the configured default in a background session and Ctrl-O lets you pick a one-off harness.

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
ww dispatch "Fix auth" --agent codex                         # one-off Codex dispatch
ww dispatch "Fix auth" --agent cursor                        # one-off Cursor dispatch

ww dispatch

Flag Description
--name Worktree/branch name (default: auto-generated from prompt)
-r, --repo Target repo by name
-b, --base Base branch to fork from
--no-fetch Skip fetching from remote
--agent Override agent.default (claude, codex, or cursor)
--yolo Run with the harness's full-access flag (claude: --dangerously-skip-permissions; codex: --dangerously-bypass-approvals-and-sandbox; cursor: --force)

Willow owns worktree creation for dispatches. Cursor's own --worktree path is not used because Willow has already created and selected the isolated worktree before launching cursor-agent.

ww cc-setup

One-time hook installation for Claude Code status tracking.

ww codex-setup

One-time hook installation for Codex CLI status tracking. Codex may ask you to review and trust the hook with /hooks.

ww cursor-setup

One-time hook installation for Cursor Agent status tracking. Willow writes direct Cursor hook entries to ~/.cursor/hooks.json, preserves unrelated hooks, and leaves hooks fail open.

ww agent setup [claude|codex|cursor|all]

Install hooks for one harness or all built-in harnesses.

ww doctor

Check your willow setup for common issues. Verifies git version, optional tools (gh, tmux, and macOS terminal-notifier), Claude Code, Codex CLI, and Cursor Agent binaries and hooks, willow directories, stale sessions, and config validity. Flags unmarked legacy Claude hooks left over from older releases.

ww doctor          # report issues only
ww doctor --fix    # prompt to remove legacy willow hooks from ~/.claude/settings.json

ww config

View, edit, and initialize willow configuration.

ww config show               # show merged config with sources
ww config show --json        # raw JSON output
ww config edit               # open global config in $EDITOR
ww config edit --local       # open local (per-repo) config
ww config init               # create default global config
ww config init --local       # create default local config

The global baseDir setting controls <willow-base> for the whole machine. It is global-only, resolved before repo-local config, and can be overridden at runtime with WILLOW_BASE_DIR.

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

ww shell-init [flags]

Print shell integration script.

Flag Description
--tab-title Set the terminal tab to repo/branch; required for notification clicks to select a non-tmux tab

Agent status

After running ww cc-setup, ww codex-setup, ww cursor-setup, or ww agent setup all, supported agents automatically report their state:

Icon Status Meaning
πŸ€– BUSY Agent is actively working
βœ… DONE Agent finished its turn
⏳ WAIT Agent is waiting for user input
🟑 IDLE Agent session ended
-- No activity detected

Status appears in ww ls, ww sw, ww status, and ww dashboard. In the tmux picker, active parent rows with one session include a harness label like [claude], [codex], or [cursor]; multi-session rows rely on the labeled child rows. 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

Config merges two tiers (local wins):

Priority Path Scope
1 ~/.config/willow/config.json Global defaults
2 <willow-base>/repos/<repo>.git/willow.json Per-repo
{
  "baseDir": "~/code/willow",
  "baseBranch": "main",
  "branchPrefix": "alice",
  "postCheckoutHook": ".husky/post-checkout",
  "setup": ["npm install"],
  "teardown": [],
  "defaults": {
    "fetch": true,
    "autoSetupRemote": true
  },
  "notify": {
    "desktop": true,
    "command": ""
  },
  "agent": {
    "default": "cursor",
    "harnesses": {
      "cursor": {
        "command": "cursor-agent",
        "args": []
      }
    }
  },
  "tmux": {
    "switcherPreview": true,
    "layout": ["split-window -h", "select-layout even-horizontal"],
    "panes": [
      { "command": "cd website" },
      { "command": "cd website" }
    ]
  },
  "telemetry": true
}

Set "tmux": {"switcherPreview": false} to hide the right-side live preview in the tmux picker and give the fzf list the full popup width. Leaving it unset keeps the preview enabled. When preview is disabled, ww tmux install prints a compact 70% by 70% popup binding instead of the default 90% by 80% binding.

Telemetry

Willow can collect anonymous error telemetry via Sentry after you opt in. This includes system errors and panic reports, plus basic context like the failing command and elapsed time. No repo contents, branch names, file paths, or personally identifiable information is sent. Each machine is identified by a hashed hostname only.

Opt out:

# Environment variable
export WILLOW_TELEMETRY=off

# Or in config (persistent)
# ~/.config/willow/config.json
{ "telemetry": false }

See the configuration docs for all options.

Terminal setup (Ghostty / iTerm / etc.)

Use --tab-title to automatically set your terminal tab to the worktree name:

eval "$(willow shell-init --tab-title)"

Each tab shows repo/branch (e.g. myrepo/auth-refactor) when inside a willow worktree.

For exact notification focus outside tmux in Ghostty, leave Ghostty's title setting unset and disable its shell integration's title updates so they do not overwrite Willow's repo/branch title:

shell-integration-features = no-title

Clickable focus requires Ghostty 1.3+ and its AppleScript integration, which is enabled by default with macos-applescript = true. macOS may ask for Automation access the first time.

Recommended Ghostty layout per worktree:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Tab: myrepo/auth-refactor           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  claude/codex    β”‚  cursor-agent    β”‚
β”‚  (agent 1)       β”‚  (agent 2)       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  shell (git diff, tests, etc.)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Contributing

# Build
go build -o bin/willow ./cmd/willow

# Test
go test ./...

# Coverage gate
GO_COVERAGE_MIN=70.0 scripts/check-go-coverage.sh

Requires Go 1.26+. fzf is bundled into the binary β€” no external fzf install needed.

Website

The docs site is built with Next.js using MDX.

cd website
pnpm install
pnpm dev       # localhost:3000
pnpm build     # production build

Deployed automatically to GitHub Pages on push to main when website/ changes.

Releasing

Releases are automated via GoReleaser and GitHub Actions.

Credits

Logo by @galenzip.

License

MIT

About

a worktree manager with agent status tracking

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages