[BUG] WorktreeCreate hook does not keep the session transcript at the launch directory (docs say it does)

Status Open
Reported on v2.1.207
Maintainer reply None cached
Activity 4 comments · opened Jul 14, 2026

Summary

The worktrees docs state:

As of v2.1.198, entering or exiting a worktree also relocates the session transcript to that directory's project storage, the same way /cd does, so /desktop and --resume find the session there afterward. Worktrees created by a WorktreeCreate hook are excluded and keep the transcript at the launch directory.

That exclusion does not happen. A worktree created by a WorktreeCreate hook has its transcript relocated to the worktree's project storage exactly like a default one, leaving the launch directory with zero transcripts — even though the app itself records the worktree as "hookBased": true.

This matters because the transcript's project slug is what scopes a session list. When a session's transcript is re-filed under the worktree's slug, the session disappears from the session list of the directory it was started in. The documented WorktreeCreate exclusion is the only advertised way to opt out of that, so it is load-bearing for anyone running multiple sessions per repo.

Environment

  • Claude Code 2.1.207, native install
  • Windows 11, git 2.x, pwsh 7

Repro

mkdir repo && cd repo
git init -b main . && echo x > a.txt && git add a.txt && git commit -m init

Add a project-scoped WorktreeCreate hook in repo/.claude/settings.json that creates the worktree itself and prints its path:

{
  "hooks": {
    "WorktreeCreate": [
      { "hooks": [ { "type": "command", "command": "bash /abs/path/to/wtcreate.sh" } ] }
    ]
  }
}
# wtcreate.sh — reads {..., "name": "..."} on stdin, prints the created worktree path on stdout
payload=$(cat)
name=$(printf '%s' "$payload" | jq -r .name)
repo=$(printf '%s' "$payload" | jq -r .cwd)
wt="$repo/.claude/worktrees/$name"
git -C "$repo" worktree add -q -b "hookwt-$name" "$wt" >&2
echo "$wt"

Then:

claude -p --worktree wt-hooked "Reply with exactly: OK"

Expected

The transcript stays at the launch directory's project storage — i.e. under the slug for .../repo.

Actual

The transcript is filed under the worktree's slug. The launch directory's slug is created but holds no transcript:

projects/…-repo                                  0 transcript(s)
projects/…-repo--claude-worktrees-wt-hooked      1 transcript(s)   <-- the whole conversation

~/.claude.json shows the app knew it was hook-created:

"activeWorktreeSession": {
  "originalCwd": ".../repo",
  "worktreePath": ".../repo/.claude/worktrees/wt-hooked",
  "sessionId": "…",
  "hookBased": true
}

Variants tested — all three relocate

  1. Plain --worktree (no hook) → transcript under the worktree slug. (Expected; this is the documented behaviour.)
  2. WorktreeCreate hook returning a path inside .claude/worktrees/ → transcript under the worktree slug.
  3. WorktreeCreate hook returning a sibling path outside .claude/worktrees/ (e.g. ../repo-wt) → transcript under the worktree slug.

In all three, the launch directory ended up with zero transcripts.

Notes

ExitWorktree with action: "keep" does move the transcript back to the launch directory — so the relocation is symmetric and reversible. The loss only becomes permanent when a session ends while still inside the worktree. That is a useful escape hatch, but it isn't the documented one.

Either the exclusion is not implemented, or the docs describe an intent that didn't ship. Happy to test a fix.

View original on GitHub ↗

4 Comments

0xbrainkid · 1 month ago

This is a good example of why transcript location is not just storage plumbing. It becomes part of the session's identity boundary.

For debugging and future-proofing, I would separate three facts in the record:

  • launch identity: original cwd/project slug where the session was created
  • execution identity: current worktree path, branch, hook source, and whether the worktree was hook-created
  • transcript custody: where the transcript is stored now, why it moved, and what operation is allowed to move it back

That makes the documented exclusion testable without depending on path conventions. A hook-created worktree can still be the execution surface, while the launch project remains the custody anchor for discovery and resume.

Two cases seem worth adding as regression fixtures:

  • hook-created worktree exits normally while still active, and the transcript remains discoverable from the launch project
  • ExitWorktree with keep restores custody explicitly, with a receipt that names both the old and new transcript slugs

The failure mode here is especially sharp because the transcript is intact but becomes invisible from the user's expected project scope. In AgentFolio/SATP terms, this is the same distinction we keep drawing in public: identity should provide continuity across handoff or relocation, while provenance should explain which boundary changed and why.

lgibelli · 1 month ago

Reproduced on Linux with Claude Code 2.1.218 (native install, bash), so this isn't Windows specific, and it's still present

wshallwshall · 1 day ago

Marked stale, but there is third-party confirmation on this thread that postdates the original report: a second reporter reproduced it on Linux with Claude Code 2.1.218 (native install, bash), so it is not Windows-specific and it was still present then.

The core claim is unchanged, and it is checkable from the documentation alone. The worktrees docs say worktrees created by a WorktreeCreate hook keep the transcript at the launch directory. They do not. The transcript is relocated to the worktree's project storage exactly like a default worktree, leaving the launch directory with zero transcripts, even though the app records the worktree as hookBased.

Either the behaviour or the documented exclusion is wrong. Right now the two disagree, and anyone relying on the documented guarantee loses their transcripts from where they expect to find them.

BasedGPT · 22 hours ago

The transcript being intact under the worktree slug is the useful distinction here. The hook-created worktree is changing transcript custody even though hookBased: true is recorded, so the launch project's session list has no file to discover. ExitWorktree with action: "keep" confirms that the move can be reversed, but it does not cover a session that ends inside the worktree.

I built BasedGPT/claude-code-session-recovery for this file-layer split. From that checkout, run python tools/diagnose.py first and follow the exact command it prints. It will compare the launch and worktree project slugs, the transcript path, and the metadata pointer before suggesting any change.

Keep byte-for-byte copies of both project directories and the relevant metadata before applying anything. If the diagnosis identifies this custody mismatch, review its dry-run output and preserve the transcript hash; if it does not, leave the files unchanged. The upstream hook exclusion still needs fixing, but this gives you a way to confirm whether the session is intact and identify the supported recovery route.

Hope this helps, if my tools are able to help you, would appreciate a ⭐ :)