Worktree
Create a new git worktree + branch and make it the working directory for the session.
Why
Worktrees keep a session's edits isolated from the root checkout, which may have its own uncommitted state or be driven by another process (an orchestrator, a watcher, a colleague's terminal). All edits, commands, and commits for the isolated task happen inside the new worktree — never in the root checkout.
Resolving <root> and <base>
- Root:
git rev-parse --show-toplevelfrom the current directory. If you are already inside a worktree, the main checkout is the first entry ofgit worktree list. - Base branch:
worktree.baseBranchfrom config if set; otherwisegit symbolic-ref --short refs/remotes/origin/HEADwith theorigin/prefix stripped; if that fails,mainif it exists, elsemaster. Only use another base if the user explicitly says so.
ROOT=$(git rev-parse --show-toplevel)
BASE=$(git -C "$ROOT" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
: "${BASE:=$(git -C "$ROOT" show-ref -q --verify refs/heads/main && echo main || echo master)}"
Steps
1. Pick the name
Derive a short kebab-case branch name from the task the user described (e.g.
feat/login-form, fix-search-pagination). If there's no task yet, use a dated name like
wip-2026-01-15. The worktree directory uses the same name with / replaced by -.
2. Make sure the base is current
git -C <root> fetch origin <base> 2>&1 | tail -3
git -C <root> log --oneline -1 origin/<base>
Branch off origin/<base> rather than the local branch — the local one may be behind.
3. Create the worktree + branch
Worktrees live under <root>/<worktreesDir> (default .claude/worktrees). First make sure
that path is gitignored (one-time): if .gitignore has no <worktreesDir>/ entry, add one —
otherwise the worktree shows up as an untracked directory in the root checkout.
cd <root> && git worktree add -b <name> <worktreesDir>/<dir-name> origin/<base>
This creates branch <name> at the base's tip and checks it out into the new worktree dir.
4. Confirm
cd <root>/<worktreesDir>/<dir-name> && git status -sb && git log --oneline -1
Working in the worktree afterwards
The shell cwd resets to the session's configured primary directory after every command —
it does NOT stay in the new worktree. So you cannot rely on a one-time cd. For every
subsequent command, prefix with the worktree path:
cd <root>/<worktreesDir>/<dir-name> && <command>
Use absolute paths under the worktree for file reads/edits. Tell the user the worktree path and branch once, then keep all work there for the session.
Before running tests, dev servers, or compose stacks from a worktree
A fresh worktree is a clean checkout: it has everything git tracks and nothing else. Each missing piece fails with an unrelated-looking error, so do these up front:
- Install dependencies — there is no
node_modules(<tool>: command not found). Detect the package manager from the lockfile (pnpm-lock.yaml→pnpm install --frozen-lockfile,yarn.lock→yarn install --frozen-lockfile,package-lock.json→npm ci,bun.lockb→bun install). JVM repos: run the wrapper once (./gradlew/./mvnw) to warm the daemon and pull dependencies. - Copy gitignored env files from the root checkout —
.env.local,.env.*.local, and anything else the app reads that git doesn't track. Symptoms: "X must be set", missing DB URLs, failing ORM/migration CLIs. - Pin the docker-compose project name.
docker composederives its project name from the cwd basename, so from a worktree it tries to bring up a second copy of every stack. Stacks that bind fixed host ports then fail withBind for 0.0.0.0:<port> failed: port is already allocated, and partial bring-ups leave orphaned containers. Decide per stack:- Share it (databases, mocks, brokers with fixed ports):
export COMPOSE_PROJECT_NAME=<composeProjectPrefix>so the worktree reuses the root checkout's containers. - Isolate it (only if the compose file maps no fixed host ports):
COMPOSE_PROJECT_NAME=<composeProjectPrefix>-<dir-name>.
- Share it (databases, mocks, brokers with fixed ports):
- Give the worktree its own dev-server port. Two worktrees cannot both bind the default
port. If
svcis installed, register the worktree as its own service via/svc:dev-servicesso it gets its own port; otherwise passPORT/-pexplicitly. A per-worktree port breaks Playwright'sreuseExistingServerunlessplaywright.config.*reads the port from env — if it doesn't, let Playwright spawn its own server rather than pointing it at the root checkout's. - Run e2e with one worker when sharing a database (
--workers=1or the project's equivalent env var). Parallel workers from two worktrees against one DB produce flaky, order-dependent failures. A fresh worktree also has cold build caches, so the first run is slower — don't read that as a hang.
Notes
- Package-manager and build-tool commands run from the project root — inside the worktree,
that root is the worktree's own directory, so
cd <worktree> && pnpm …is correct. - To remove a worktree later (only when asked):
git -C <root> worktree remove <worktreesDir>/<dir-name>(add--forceif it has untracked files such asnode_modules), thengit branch -d <name>if the branch is done. - Do not commit unless the user explicitly asks.
Configuration
Optional overrides in .claude/claude-skills.json under the worktree key:
{
"worktree": {
"baseBranch": "develop",
"worktreesDir": ".claude/worktrees",
"composeProjectPrefix": "myapp"
}
}
| Key | Default | Meaning |
|---|---|---|
baseBranch |
origin/HEAD → main → master |
Branch new worktrees branch off |
worktreesDir |
.claude/worktrees |
Directory (relative to root) that holds worktrees; must be gitignored |
composeProjectPrefix |
repo directory basename | COMPOSE_PROJECT_NAME used when sharing compose stacks; suffixed with the worktree name when isolating |
To change this skill, do not edit this copy: use /dev-tools:update-skill, or see docs/updating-skills.md in mzvonar/claude-skills-public.