cd /news/ai-tools/claude-code-deletes-clean-worktrees-… · home topics ai-tools article
[ARTICLE · art-127313] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↓ negative

Claude Code deletes clean worktrees on exit without asking. git worktree lock stops it.

A developer found that Claude Code 2.1.263 silently deletes a clean, unnamed worktree and its branch on /exit without prompting, running git worktree unlock, git worktree remove --force, and git branch -D. The cleanup routine only preserves worktrees whose lock reason matches its own internal regex, so re-locking the worktree with a user-set reason via git worktree lock prevents the deletion. The finding was tested on git 2.52.0 on macOS.

by read6 min views4 publishedSep 11, 2026

/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:

── more in #ai-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-code-deletes-…] indexed:0 read:6min 2026-09-11 ·