worktree is a CLI for people who use Git worktrees as part of their daily development flow and do not want to keep typing the same setup, cleanup, and editor-opening commands over and over.
It wraps the most common worktree tasks into a small workflow-oriented tool:
- create a new worktree from your default base branch
- check out an existing remote branch into its own worktree
- copy local env files into the new worktree
- open the result in your editor automatically
- hand the new worktree straight to a coding agent
- list, reopen, remove, and clean up worktrees later
This README focuses on the fast path. The documentation website will cover deeper examples, advanced workflows, integrations, and troubleshooting.
Docs: https://northguild.github.io/worktree
Git worktrees are great when you want multiple branches checked out at once, but the raw commands are still a bit awkward for everyday use. In practice, teams usually want a repeatable flow like this:
- branch off
origin/main - create a sibling worktree directory
- copy
.envfiles - open it in the editor
- clean up stale worktrees later
worktree turns that into a few commands with sensible prompts.
By default, worktrees are created in a sibling folder named <repo>.worktrees, so your main repository stays clean while related worktrees stay easy to find.
One of the biggest wins with worktrees is how fast context switching becomes.
Instead of juggling one checkout and constantly doing this dance:
- stash current changes
- switch branches
- do quick fix
- switch back
- unstash and resolve surprises
you keep each task in its own directory and jump between them directly.
That means:
- fewer stash/unstash cycles
- less risk of stash conflicts or forgotten stashes
- less accidental cross-branch contamination
- faster interrupts, reviews, and hotfixes
In short: stop paying a context-switching penalty and stop stashing just to move between tasks.
npm install -g @northguild/worktreeOr run it without a global install:
npx @northguild/worktree --helpTo teach a coding agent how to drive worktree, including its --json agent mode, add the usage skill:
npx skills add northguild/worktree --skill worktreeAt a terminal the command asks which agent(s) to install the skill into; run from inside a coding agent
it installs without asking, and --agent <name> chooses. It installs into the current project; add
--global (-g) to install it for your user instead, so the agent has it in every repository. It is
separate from, and in addition to, npm install -g: the skill teaches an agent the CLI, it does not
install the CLI.
Run the initial configuration once inside a Git repository:
worktree configThe setup flow can configure:
defaultSourceBranchfor new worktrees, such asorigin/maincodeEditorfor automatically opening a worktree, such ascodeopenerfor where a worktree opens —editor(default),herdrornoneagent.commandfor the command--agentruns to hand a brief to a coding agent, such asclaude --bgpostCreatefor a command to run in each new worktree, such aspnpm install(otherwise inferred from the lockfile)
Then create your first worktree:
worktree branch feature/improve-readmeThat will:
- create a new branch from your configured source branch
- add a Git worktree under
<repo>.worktrees/feature/improve-readme - copy the gitignored env files from the main repository —
.env*,.dev.vars*and.envrc - install dependencies — by default when non-interactive, and on a terminal when
postCreateis set or--installis given (--no-installskips it); the command ispostCreate, else inferred from the lockfile (pnpm-lock.yaml,package-lock.json,yarn.lock,bun.lock), and a failure keeps the worktree, opens nothing and exits1 - open the new worktree in your configured editor, if one is set — or as a Herdr space when
openerisherdr
worktree branch feature/add-bulk-actionsCreate from a different source branch:
worktree branch feature/add-bulk-actions --source origin/release/1.4worktree checkout feature/fix-login-timeoutYou can also pass the full remote name:
worktree checkout origin/feature/fix-login-timeoutThis creates a local tracking branch in a dedicated worktree.
worktree branch feature/add-bulk-actions --agent "add bulk actions to the table"The agent starts with the new worktree as its working directory and the flag's value as its prompt, so it works inside <repo>.worktrees alongside everything else. worktree checkout takes --agent too.
There is one agent per worktree. With opener set to herdr, Herdr starts it in the new space and the prompt is submitted to it afterwards (a claude agent is named <repo>-<branch>, printed on stderr); the kind is herdr.agent, or the program agent.command names. Otherwise agent.command is launched detached. A brief with no agent to take it exits 2 before creating anything. Without a brief, only herdr.agent starts an agent.
For a long prompt, --agent-file <path> or --agent-stdin reads it whole (at most 131,071 bytes, the most one argument can carry on Linux; the three are mutually exclusive). --no-agent opens the worktree without an agent, and --no-open creates it and prints its path without opening anything.
worktree listTo name the agent session living in each worktree:
worktree list --agentsSessions are found from two places, joined on the directory they run in: herdr agent list when herdr is installed, and the runtime's own <program> agents --json, where the program is agent.command's first word or else herdr.agent. A finished session shows [done], unless Herdr still shows it in a pane or its process is still running. With neither source available the list simply shows no sessions.
worktree open feature/add-bulk-actionsIf you omit the branch name, the CLI shows an interactive picker.
worktree remove feature/add-bulk-actionsForce removal when you already know what you are doing:
worktree remove feature/add-bulk-actions --forceAliases are also available:
worktree rm feature/add-bulk-actionsWith opener set to herdr, removing a worktree also closes the Herdr space it was opened as.
worktree cleanupThe cleanup command targets worktrees that are considered safe to remove, for example branches whose remote no longer exists, and local worktrees with no tracked remote. Either way the worktree has to be carrying nothing — no uncommitted changes, and no commits that have not been pushed. A commit count that could not be taken is never read as a zero, so a worktree whose directory still exists is held back rather than swept when it cannot be checked.
A worktree that an agent session is working in is held back and reported as skipped. A session that is only idle does not hold anything back. That includes a session Herdr started for you, found through herdr agent list, as well as one in your own terminal. --force does not override that, because it answers the confirmation prompt rather than the safety verdict; --ignore-agents is the flag that does.
With opener set to herdr, every worktree removed here also has its Herdr space closed. The ones held back keep theirs — cleanup closes what it deleted, not what it looked at. Note that --ignore-agents therefore also closes a live agent's space, taking its panes down with the directory.
| Command | What it does |
|---|---|
worktree config |
Configure defaults like source branch and editor |
worktree branch <name> |
Create a new branch in a new worktree |
worktree checkout <remote-branch> |
Check out an existing remote branch into a worktree |
worktree list |
List known worktrees |
worktree open [branch] |
Open an existing worktree in your editor |
worktree remove [branch] |
Remove one or more worktrees |
worktree cleanup |
Remove stale worktrees that are safe to delete |
For command help at any time:
worktree help
worktree help branchConfiguration is stored in local Git config under the northguild.worktree.* namespace.
Examples:
worktree config defaultSourceBranch origin/main
worktree config codeEditor code
worktree config opener herdr
worktree config agent.command "claude --bg"
worktree config --list
worktree config --missingcodeEditor is the executable plus any arguments, run without a shell — quotes group, but ~ and
$VAR are not expanded. Set opener to none to open nothing — branch then prints
Worktree created at <path> and stops, which suits scripts and agents. worktree config <key> with no
value prints the stored value. Set opener to herdr to open worktrees as
Herdr spaces instead of editor windows — and to close those spaces again when
remove or cleanup deletes the worktree; herdr.focus and herdr.agent tune that. See the
configuration docs.
branch, list and remove can run with nothing at the keyboard — from a script, CI, or another coding
agent — and hand back one JSON document to parse. Nothing in this mode prompts, animates or waits without a
bound. A run on a terminal behaves exactly as described above.
A run is non-interactive when any of these hold:
- stdin is not a terminal
CIis set and is not empty,0orfalse--non-interactiveor--yes(-y) is given — every command accepts both--jsonis given, onbranch,listorremove
A prompt that has a default takes it. One that has none fails at once with exit 2 and a single stderr
line, worktree: no default for <value>; pass <flag>.
| Flag | On | Does |
|---|---|---|
--json |
branch, list, remove |
one JSON document on stdout; everything for a person goes to stderr |
--agent <text>, --agent-file <path>, --agent-stdin |
branch (checkout takes --agent) |
the brief for the agent, from a value, a file or piped stdin; mutually exclusive with each other and with --no-agent, read whole, empty is refused, at most 131,071 bytes |
--no-open |
branch |
create the worktree and print its path; call neither Herdr nor the editor |
--no-agent |
branch |
open as usual, start no agent |
--install / --no-install |
branch |
force the dependency install on or off for this run |
--assign / --no-assign |
branch |
assign the --github issue to you, or not |
-f, --force |
remove |
skip the confirmation; required when non-interactive |
- Branch name from
--github:<prefix><number>-<slug>, the slug cut to 48 characters at the last dash. With no issue, the name is required. - Assignment:
--assign/--no-assign, thengithub.autoAssign, then assign. The default is never saved. - Install: on. The command is
postCreate, else inferred from the lockfile (pnpm-lock.yaml,package-lock.json,yarn.lock,bun.lock). With nothing to run it says so and carries on. A failure keeps the worktree, starts nothing and exits1. - Source branch:
defaultSourceBranch, elseorigin/mainwith a warning. - Removal has no default:
remove <branch> -f. - Bounded calls: GitHub and Jira requests 15 s,
git fetch60 s,gh auth tokenand the session listing 10 s, the install 10 min. A command still running at its bound is sent SIGTERM, then SIGKILL 2 s later, so one that ignores SIGTERM cannot hold the run open.
With --json, stdout is one line holding one document, and a count that could not be taken is null, never
0. Tokens are never printed.
worktree branch --github 42 --json --agent-file brief.md{"path":"/abs/repo.worktrees/42-fix-login","branch":"42-fix-login","source":"origin/main",
"issue":{"provider":"github","number":42,"url":"https://github.com/acme/demo/issues/42"},"assigned":true,
"envFilesCopied":["docs/.env.local"],
"installed":{"ran":true,"command":"pnpm install --frozen-lockfile","inferred":true,"ok":true},
"herdr":{"space":"w5","pane":"w5:p1","agent":"wt-42-fix-login"},
"agent":{"name":"demo-42-fix-login","kind":"claude","command":["claude","--name","demo-42-fix-login"],"prompted":true},
"warnings":[]}issueis{provider:"github",number,url},{provider:"jira",key,url}, ornull.assignedisnullwhen no assignment was attempted.herdrandagentarenullwhen skipped, andagent.commandleaves the brief out.agent.nameisnullwhere this CLI named nothing (a detached start).installedis{ran:false,reason}when skipped. A failed install still prints the whole document, withinstalled.okfalse, and exits1.- A brief that was given and not delivered does the same: the whole document,
agentnulloragent.promptedfalse, the reason inwarnings, exit1.
list --json gives {"worktrees":[{branch,path,current,pathExists,remote,remoteExists,ahead,behind,mergedInto,uncommittedChanges,safeToRemove}]}.
With --agents, each entry also has agent: null, or {name,sessionId,herdrAgent,live,interactive,waiting}.
herdrAgent is the name Herdr gives the agent (for example wt-42-fix-login, the same as branch --json's herdr.agent), or its pane id (for example w4P:p1) when Herdr reports no name.
remove --json gives {"removed":[{branch,path}],"herdrSpacesClosed":[…],"warnings":[]}. Nothing removed is
never reported as a success.
A failure prints {"error":{"code","message",…}} on stdout, one line on stderr, and exits non-zero:
code |
Means | Exit |
|---|---|---|
missing_value |
a value with no default was not given | 2 |
invalid_value |
a value or flag combination was refused | 2 |
not_found |
something named does not exist | 2 |
timeout |
a bounded call did not answer in time | 1 |
failed |
anything else | 1 |
Exit codes follow the same rule without --json: 0 success, 1 failure, 2 a usage or value problem.
A human's Ctrl-C at a prompt stays silent and exits 0.
With opener herdr, Herdr starts the agent in the new space and the brief is submitted afterwards with
herdr agent prompt, never inside the start command, so no shell sees it. The agent kind is herdr.agent;
when a brief is given and that is unset, the program agent.command names. agent.command's arguments are
reused without --bg, and a claude agent gets --name <repo>-<branch>, lowercased and never truncated.
Otherwise agent.command is launched detached. --no-open and
opener none open nothing and still start the detached agent when a brief is given.
A brief that no configured agent would take — no herdr.agent or agent.command for Herdr, no
agent.command for the detached start — exits 2 with missing_value before anything is created.
A brief that still reaches no agent once the worktree exists (Herdr did not open and there is no
agent.command to fall back to, the space was already open, the agent did not start, the prompt failed)
prints the whole document with the reason in warnings and exits 1. agent.prompted is the delivery
receipt: check it, not just the exit code, when a brief matters.
list --agents asks two sources, and either may be missing:
herdr agent list, whenherdris onPATH.- The runtime's own
<program> agents --json, where the program isagent.command's first word, elseherdr.agent.
They are joined on the directory each session runs in, compared as real paths, and a Herdr entry is named by
the runtime session with the same session id. A session without a pid is kept. live is false for a
session the runtime reports as finished ([done] in the text list), unless Herdr still shows it in a pane or the
process behind it is still running. With
neither source, the result is no agents and no error. cleanup holds a worktree back for any session that is still live and not just idle; a finished or idle one does not hold it back. A session counts as idle when the runtime's status or Herdr's status is idle (or Herdr's done) and none of the statuses it has says otherwise. waiting is true for a live session that is not progressing: idle between turns or blocked on a question, so probably waiting on you. It applies to an interactive session too, such as an agent worktree branch started through Herdr, and to one the runtime calls finished while its process still runs, which is weighed like any other live session.
A coordinating agent — Claude Code, say — can fan work out to one worktree each:
- Run
worktree branch --github N --json --agent-file brief.mdper issue. - Make the brief name the coordinating session and say its follow-ups carry the user's authority. Without that, a Claude session treats messages from other sessions as information, not instructions.
- Set
agent.commandwith a permission mode compatible with the coordinator's. Claude Code can hold a cross-session message for its user's approval when the two sessions' modes differ. In the check below, a worker in the default mode was not held when messaged from a coordinator in auto mode. - Read
agent.namefrom the document to address the session, andworktree list --agents --jsonto check it is stilllive.waitingturnstruewhen the worker finishes its turn or stops on a question, so it is the signal to poll. - Finish with
worktree remove <branch> -f --json.
Run on 2026-10-01 from Claude Code's Bash tool (no TTY, stdin closed) against this repository and a
disposable issue, with opener herdr, herdr.agent claude, Herdr 0.9.0, Claude Code 2.1.286, Node 24.19.0
on macOS:
| Command | Exit | Time |
|---|---|---|
worktree branch --github <issue> --json --agent-file brief.md |
0 |
18.6 s, 6.8 s of it the install |
worktree list --agents --json |
0 |
3.6 s |
worktree branch --json (no name) |
2 |
0.4 s |
worktree remove <branch> -f --json |
0 |
12.4 s |
The first created the tree, installed, opened a Herdr space and started claude, which received the brief and
replied; the last removed the tree, the branch, the space and the session. The third printed
{"error":{"code":"missing_value",…}} and exactly one stderr line. Nothing prompted and nothing hung.
A second run the same day checked the coordinator's side of the handshake. worktree branch msg-check --json --agent-file brief.md (exit 0, 14.8 s) started claude as worktree-msg-check, the agent.name the document
reported, with a brief naming the coordinating session. That name appeared in the coordinator's list of local
Claude sessions. A message sent to it by that name was answered within seconds: the agent ran
git branch --show-current in its tree and sent the result back to the coordinator by name. The message was not
held for approval, with the worker in the default permission mode and the coordinator in auto mode. worktree remove msg-check -f --json (exit 0, 11.2 s) then removed the tree, the branch, the space and the
session.
The per-command pages have the detail: branch,
list,
remove and
Herdr spaces.
The README is intentionally optimized for onboarding and everyday usage.
The documentation website should be the place for:
- in-depth walkthroughs
- team conventions and naming strategies
- integration guides
- edge cases and troubleshooting
- richer examples for different repository layouts
A change to the published package (src/**, bin/**, skills/**, or package.json dependencies) needs a release note. Run:
pnpm changeset:addPick the bump level (patch, minor, or major for a breaking change) and write the description for someone installing the package: it becomes the CHANGELOG.md entry and the GitHub release text. Commit the .changeset/*.md file it writes with the change. A pull request check (pnpm changeset:status) fails when the package changed and no note was added.
Merging a pull request publishes nothing. A release is a deliberate release pull request: a maintainer runs pnpm changeset:prepare-release, which bumps the version, writes CHANGELOG.md and consumes the notes; the maintainer then commits and opens the pull request. Merging it tests, builds and publishes to npm, tags v<version> and creates the GitHub release.
- Git installed and available on your
PATH - Node.js available to run the CLI
- an existing Git repository where you want to manage worktrees
- macOS or Linux — Windows is not supported
If you want automatic editor launching, make sure your editor command is available on your PATH, for example code for Visual Studio Code.
If you want worktrees to open as Herdr spaces, make sure the herdr CLI is on your PATH and its server is running.
MIT
- Add
configcommand to configure everything needed - Add
branchcommand to create new worktrees - Add
removecommand to delete worktrees - Add
checkoutcommand to create worktree from a remote branch - Add
listcommand to list all worktrees - Add
opencommand to open a worktree in a code editor - Add
cleanupcommand to cleanup stale worktrees - Integrate with GitHub for automated branch naming
- Integrate with JIRA for automated branch naming
- Integrate with ClickUp for automated branch naming
