/exit a claude --worktree <name> session with git worktree lock on the worktree and only ever releases locks whose reason matches its own format. SessionStart hook can do the re-lock automatically.git worktree add does worktree-<name>, existing unmerged commits get force-deleted too.
Tested on Claude Code 2.1.263, git 2.52.0, macOS.
You start an isolated session:
claude --worktree wt-clean
You read some code, maybe run a few commands, and then /exit. You never picked "Remove worktree". But:
$ git worktree list
/path/to/demo 731024a [main]
If you blink you see Cleaning up worktree (no pending changes)… flash by. That's the only notice you get.
I pulled the bundled source out of the Claude Code binary to see the decision. On /exit from a worktree session it runs two git commands inside the worktree:
git status --porcelain # changed + untracked files
git rev-list --count <HEAD at session start>..HEAD # new commits this session
Then it branches like this:
| Worktree state | Session named? | What /exit does |
|---|---|---|
| 0 changed files, 0 new commits | no | Removes worktree and branch, no prompt |
| 0 changed files, 0 new commits | yes ( /rename ) |
Keep / Remove dialog |
| Any uncommitted change or any new commit | either | Keep / Remove dialog |
The silent path runs:
git worktree unlock <path>
git worktree remove --force <path>
git branch -D worktree-<name>
The "new commits" check is relative to the HEAD at session start. Anything committed before the session doesn't count. Keep that in mind for the pre-created worktree gotcha below.
From the official worktrees page:
The worktree is clean: for an unnamed session, Claude removes the worktree and its branch automatically. A named session prompts you first so you can keep the worktree for later
So the design intent is "clean means disposable". Plenty of people disagree. Related issues (all closed):
There is no worktree.keepOnExit-style setting. So we lean on git instead.
Claude Code already uses git worktree lock on every worktree it creates. Look at the registry while a session is running:
$ git worktree list --porcelain
worktree /path/to/demo/.claude/worktrees/wt-lock
HEAD 731024acd920b9883ae2f00845fa4ebc8ae0230e
branch refs/heads/worktree-wt-lock
locked claude session wt-lock (pid 72411 start Tue Sep 8 13:50:41 2026)
At exit, the cleanup routine reads the lock reason back and tests it against this regex (from the bundle):
/^claude (?:agent|session) .{1,255} \(pid (\d{1,10})(?: start (.{1,255}))?\)$/
If the reason doesn't match, or it matches but the PID belongs to another live process, the routine keeps the worktree and logs:
cleanupWorktree: kept <path> — locked by another live Claude Code process, or with a reason we did not write
The docs say the same thing about the background stale-lock sweep: "The sweep never releases a lock you set yourself with git worktree lock." A user-set lock is consistently treated as off-limits.
During the session, from another terminal (or with the ! prefix inside Claude Code):
git worktree unlock .claude/worktrees/wt-lock
git worktree lock --reason "pinned by ryan" .claude/worktrees/wt-lock
You must unlock first. Claude Code's lock is already there, so a bare lock fails:
$ git worktree lock --reason "keep" .claude/worktrees/wt-lock
fatal: '.claude/worktrees/wt-lock' is already locked, reason: claude session wt-lock (pid 72411 start ...)
Now /exit:
$ git worktree list
/path/to/demo 731024a [main]
/path/to/demo/.claude/worktrees/wt-lock 731024a [worktree-wt-lock] locked
The reason text doesn't matter. Running git worktree unlock . && git worktree lock . from inside the worktree, with no reason at all, also kept it.
Next time, claude --worktree wt-lock drops you back into the same worktree. Your lock stays, so it survives the next /exit too.
Save this script:
#!/bin/sh
git_dir=$(git rev-parse --git-dir 2>/dev/null) || exit 0
common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
[ "$git_dir" = "$common_dir" ] && exit 0 # main worktree: nothing to pin
wt=$(git rev-parse --show-toplevel)
git worktree unlock "$wt" 2>/dev/null # drop Claude Code's own lock
git worktree lock --reason "pinned by SessionStart hook" "$wt"
In the main worktree --git-dir and --git-common-dir are the same path, so the script is a no-op there. In a linked worktree it swaps the lock.
Register it:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "sh \"$CLAUDE_PROJECT_DIR/.claude/hooks/pin-worktree.sh\"" }
]
}
]
}
}
Right after claude --worktree starts:
$ git worktree list --porcelain | grep locked
locked pinned by SessionStart hook
/exit leaves the worktree in place, still locked.
This tripped me up, so here's what I measured:
| Settings location | Hook fires in the worktree session? |
|---|---|
.claude/settings.json committed to the repo |
yes (it's in the worktree checkout) |
Untracked .claude/settings.local.json only in the main checkout |
no (the worktree checkout doesn't have it) |
A settings file outside the repo (passed with --settings ) |
yes |
~/.claude/settings.json (user settings) |
yes |
The worktree session reads project settings from the worktree's own checkout. If you don't want to commit anything, put the hook in your user settings at ~/.claude/settings.json with an absolute script path. The script has no repo-specific logic, so it's safe globally.
Side note from the docs: $CLAUDE_PROJECT_DIR keeps pointing at the main checkout after Claude enters a worktree, while the hook's cwd follows the worktree. That's why the script derives the path from git rev-parse instead of the env var.
I assumed a worktree I made myself would be left alone. It wasn't. claude --worktree <name> adopts an existing .claude/worktrees/<name> and applies the same exit logic.
Worse: if the branch is named worktree-<name>, commits made before the session don't count as "new", so an unmerged branch gets force-deleted:
$ git worktree add -b worktree-wt-pre4 .claude/worktrees/wt-pre4
$ cd .claude/worktrees/wt-pre4 && echo precious > precious.txt && git add . && git commit -m "precious"
$ claude --worktree wt-pre4 # do nothing, /exit
$ git branch --list worktree-wt-pre4
The commit object still exists, so git fsck --lost-found can dig it out, but only if you notice. If you hand a hand-made worktree to Claude Code, lock it first.
A named session always gets the dialog, even when clean:
Exiting worktree session
This session was named "keepme". Keep the worktree to resume it later, or remove it to clean up.
❯ 1. Keep worktree
2. Remove worktree
/rename keepme then /exit is the cheapest way to keep a worktree once. You have to remember it every time, which is why I prefer the hook.
Claude Code won't touch a worktree you locked, so removal is on you:
git worktree unlock .claude/worktrees/wt-lock
git worktree remove .claude/worktrees/wt-lock
git branch -D worktree-wt-lock
Claude Code's worktree cleanup is aggressive by design: clean plus unnamed equals gone. The escape hatch is already built in, just not advertised. Claude Code refuses to release a git worktree lock it didn't write, so a re-lock with your own reason, done by hand or by a SessionStart hook, is enough to keep the worktree across sessions.
Links: