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

> Source: <https://dev.to/day_b3fa2204948ded3059f6f/claude-code-deletes-clean-worktrees-on-exit-without-asking-git-worktree-lock-stops-it-ej>
> Published: 2026-09-11 23:12:29+00:00

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

``` bash
$ git worktree list
/path/to/demo  731024a [main]
# .claude/worktrees/wt-clean is gone, and so is branch worktree-wt-clean
```

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:

``` bash
$ 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:

``` bash
$ 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`:

``` bash
$ 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:

``` bash
#!/bin/sh
# .claude/hooks/pin-worktree.sh
# Keep Claude Code from auto-removing this linked worktree on exit.
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:

``` bash
$ 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:

``` bash
$ 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
# empty. No ref points at that commit any more.
```

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:
