Skip to content

Recognize agent worktrees in transcript, settings, and session lookup#1159

Open
matthiaswenz wants to merge 7 commits into
mainfrom
fix/claude-worktree-transcript-resolution
Open

Recognize agent worktrees in transcript, settings, and session lookup#1159
matthiaswenz wants to merge 7 commits into
mainfrom
fix/claude-worktree-transcript-resolution

Conversation

@matthiaswenz
Copy link
Copy Markdown
Contributor

@matthiaswenz matthiaswenz commented May 8, 2026

Summary

Fix dropped checkpoints when Claude Code runs from inside its .claude/worktrees/<branch>/ feature. Three independent issues conspired to make agent work in linked worktrees invisible to Entire — fixing only one wasn't enough. This PR addresses all three.

The user-visible symptom: a Claude session that does work in a linked worktree finishes cleanly, but the commit lands without an Entire-Checkpoint trailer, no metadata appears on entire/checkpoints/v1, and entire status from inside the worktree reports not set up.

What was wrong

1. Transcript path mismatch. Claude Code reports transcript_path encoded from the worktree CWD, e.g. ~/.claude/projects/-Users-foo-Development-repo--claude-worktrees-feature/<id>.jsonl, but the file is actually written under the parent repo's project dir at ~/.claude/projects/-Users-foo-Development-repo/<id>.jsonl. The TurnEnd handler aborted at the file-exists check, so no shadow-branch commit was created.

2. .entire/ resolved against the wrong root. paths.AbsPath(".entire/settings.json") used git rev-parse --show-toplevel, which from inside a linked worktree returns the worktree dir — not the main repo where .entire/ actually lives. Every git hook firing from inside a linked worktree saw IsSetUpAndEnabled = false and silently bailed. entire status from the worktree reported not set up.

3. Session matching was strict-equal on WorktreePath. Even with the gate unblocked, findSessionsForWorktree couldn't find the active session: UserPromptSubmit fired from the main worktree (so the session's WorktreePath is the main repo), but prepare-commit-msg / post-commit fire from inside the linked worktree where the commit happens. Strict equality returned no matches, and hooks logged no active sessions at DEBUG and bailed.

Resolution

Transcript fallback (cmd/entire/cli/agent/claudecode/lifecycle.go).
When the reported path doesn't exist, the resolver strips the trailing --claude-worktrees-<branch> marker from the project segment and stats the parent-repo candidate. Deterministic — no directory scanning, no chance of crossing into an unrelated project that happens to share a session ID. strings.LastIndex is used (not Index) so repos whose sanitized root already contains the token (e.g. checked out under acme--claude-worktrees-tools/) only have the trailing synthetic marker stripped. Gates: triggers only on os.IsNotExist, only when the reported path is under ~/.claude/projects/, only when the project segment carries the marker; session ID validated before filepath.Join; candidate must exist before being returned.

.entire/ anchored at the main worktree (cmd/entire/cli/paths/paths.go, cmd/entire/cli/logging/logger.go).
New paths.MainWorktreeRoot derives the main repo root from git rev-parse --git-common-dir's parent. paths.AbsPath now routes any .entire/* relative path through it; all other paths still resolve against WorktreeRoot. logging.Init likewise uses MainWorktreeRoot so hook log output writes to the canonical .entire/logs/entire.log instead of spawning an orphan .entire/logs/ inside each linked worktree.

Session lookup widens linked → main (cmd/entire/cli/strategy/manual_commit_session.go).
findSessionsForWorktree now also accepts sessions whose WorktreePath is the main worktree when invoked from a linked worktree. The widening is one-directional — sibling linked worktrees still don't bleed into each other, and main-worktree lookups are unchanged — so commits on main can't accidentally pick up an unrelated agent session running in .claude/worktrees/<branch>.

Test plan

  • mise run fmt && mise run lint — clean
  • mise run test:ci — unit + integration + canary green
  • Transcript resolver unit tests: passthrough, empty inputs, no-marker, marker-at-start, marker-in-repo-root, outside-base, traversal rejection, worktree fallback
  • TestClaudeCode_WorktreePathFallback (integration): drives entire hooks claude-code stop with a fake HOME and a worktree-encoded transcript_path that doesn't exist; verifies a checkpoint is created end-to-end
  • TestMainWorktreeRoot_LinkedWorktree, TestAbsPath_EntireAnchoredAtMainWorktree, TestIsEntireRelPath: path resolution from inside a linked worktree
  • TestLinkedWorktree_EntireSetUpVisible (integration): entire status from a linked worktree reports the active configuration instead of not set up
  • TestFindSessionsForWorktree_LinkedWorktreeMatchesMain / _MainWorktreeUnchanged: widening is one-directional
  • End-to-end: built binary, ran entire status from inside /path/to/repo/.claude/worktrees/<branch> and from real prepare-commit-msg invocation; verified hook now logs prepare-commit-msg hook invoked and reaches the session-matching layer

Notes

  • Should also be reported upstream to Claude Code: transcript_path should match the file's actual storage location.
  • The inner Claude session created by Claude's worktrees feature still doesn't fire its own UserPromptSubmit because the linked worktree has no .claude/settings.json (only settings.local.json with permissions). Commits from inside the worktree therefore link to the outer session, not a dedicated sub-session. That's a separate ergonomic limitation upstream of this PR — either Claude needs to carry project hooks into worktrees, or Entire needs to install hooks at ~/.claude/settings.json.
  • The resolver and the session-lookup widening both log at INFO when they take the worktree path (agent.claudecode resolved transcript via worktree fallback), observable in .entire/logs/.

Claude Code's worktree feature reports transcript_path encoded from the
worktree CWD (.claude/worktrees/<branch>) but writes the actual transcript
under the parent repo's project dir. The TurnEnd handler aborted on the
file-exists check, so no checkpoint condensed and commits had no
Entire-Checkpoint trailer.

When the reported path doesn't exist, scan ~/.claude/projects/*/ for the
session ID and use the match. No-op when the path resolves normally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 710ca10c1821
Copilot AI review requested due to automatic review settings May 8, 2026 14:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes dropped checkpoints for Claude Code sessions when Claude’s transcript_path points to a non-existent worktree-encoded project directory by adding a fallback that scans the Claude projects base directory for the real <sessionID>.jsonl location.

Changes:

  • Resolve Claude transcript paths via a fallback scan when the reported transcript_path does not exist.
  • Add a helper to find a transcript by session ID under ~/.claude/projects/*/.
  • Add unit tests covering the resolver and scanning behavior, including the worktree mismatch scenario.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
cmd/entire/cli/agent/claudecode/lifecycle.go Adds transcript path resolution with a fallback scan and a helper to locate transcripts by session ID.
cmd/entire/cli/agent/claudecode/lifecycle_test.go Adds unit tests for the new scan helper and transcript-path resolver (including worktree fallback).

Comment thread cmd/entire/cli/agent/claudecode/lifecycle.go Outdated
Comment thread cmd/entire/cli/agent/claudecode/lifecycle.go Outdated
Comment thread cmd/entire/cli/agent/claudecode/lifecycle.go
matthiaswenz and others added 2 commits May 8, 2026 17:04
- Only fall back on os.IsNotExist; pass through other Stat errors so
  permission/IO problems aren't masked by a directory scan.
- Validate sessionID with validation.ValidateAgentSessionID before using
  it in filepath.Join, so a hostile hook payload can't traverse out of
  the projects base dir during the scan.
- Skip the scan when the reported path isn't under the Claude projects
  base dir; for any other path the scan can't produce a better answer.

Adds unit tests covering the outside-base-dir and traversal-id paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3aa3df224dc5
Entire-Checkpoint: 74920b2829ad
@Soph Soph marked this pull request as ready for review May 8, 2026 16:26
@Soph Soph requested a review from a team as a code owner May 8, 2026 16:26
matthiaswenz and others added 2 commits May 11, 2026 14:54
Git hooks fired from inside a linked git worktree (e.g. Claude Code's
.claude/worktrees/<branch> feature) silently bailed: settings.IsSetUp
resolved .entire/settings.json via show-toplevel of the linked worktree,
which has no .entire/, so IsSetUpAndEnabled returned false and
prepare-commit-msg/post-commit/pre-push all skipped their work. User
commits made inside the worktree never got an Entire-Checkpoint trailer,
were never condensed, and entire/checkpoints/v1 never advanced on push.

paths.AbsPath now routes any .entire/* relative path through a new
MainWorktreeRoot helper (parent of git --git-common-dir), keeping all
other paths anchored at WorktreeRoot. .entire/ is a main-repo concern —
linked worktrees should share the canonical config and state directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 84a26a76972c
Even after .entire/ paths were anchored at the main worktree, hooks
firing from inside an agent-managed worktree (e.g. Claude Code's
.claude/worktrees/<branch>) still silently bailed at the next layer:
findSessionsForWorktree strict-matched on WorktreePath, so a commit
made in the linked worktree could not find the session whose
UserPromptSubmit had fired from the main worktree. The user-visible
symptom: no Entire-Checkpoint trailer on the commit, no condensation,
no metadata pushed for that session.

findSessionsForWorktree now widens to also accept sessions registered
against the main worktree when invoked from a linked worktree. The
widening is one-directional — sibling linked worktrees don't bleed
into each other, and main-worktree lookups are unchanged — so commits
on main can't accidentally link to an unrelated agent session.

logging.Init also moves from WorktreeRoot to MainWorktreeRoot so hook
log output writes to the canonical .entire/logs/entire.log instead of
spawning an orphan .entire/logs directory inside each linked worktree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6f37d51c79a0
@matthiaswenz matthiaswenz changed the title Fall back to scanning when Claude transcript_path is missing Recognize agent worktrees in transcript, settings, and session lookup May 11, 2026
matthiaswenz and others added 2 commits May 11, 2026 16:08
- Drop isEntireRelPath in favor of the existing IsSubpath helper.
  Same semantics on every test input (siblings, parent escapes,
  absolute paths, look-alikes), so no behavior change — just less
  code reinventing path containment.
- Trim resolveTranscriptPath and worktreeParentCandidate docstrings:
  cut the bulleted gating recital and the WHAT-it-does narration,
  keep the WHY (Claude Code transcript_path bug, LastIndex rationale
  inline at the choice site).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b001dc12437e
findSessionsForWorktree was unconditionally widening linked → main:
from any linked worktree it returned both linked-worktree sessions AND
main-worktree sessions. That fixed Claude's worktrees-feature path
(outer session in main, inner subprocess commits from linked) but had
two unintended consequences for users who run agents in linked
worktrees independently:

  1. Bundling. A commit in `git worktree add ../wt && cd ../wt && agent`
     would silently attach any active main-worktree session to its
     checkpoint, mixing unrelated session context. The multi-session
     model is documented as "sessions in the same directory interleave"
     — different worktrees are different directories.

  2. BaseCommit corruption. postCommitUpdateBaseCommitOnly (fired when a
     commit has no Entire-Checkpoint trailer) clobbers state.BaseCommit
     to the new HEAD for every session it sees. With the widened lookup,
     a no-trailer commit in a linked worktree would advance main's
     active session's BaseCommit to a commit on the linked branch.
     Future prepare-commit-msg on main then filters that session out
     (BaseCommit != HEAD), silently killing it.

Switch to fallback-only semantics: prefer strict-equal matches, fall
back to main only when the linked worktree has no session of its own.
Preserves the Claude's-worktrees-feature fix (linked has no session →
fallback finds the outer session in main), while keeping
linked-worktree sessions and main-worktree sessions isolated when both
exist.

The fallback stays one-directional. Main-worktree lookups are unchanged
and sibling linked worktrees still never bleed into each other.

Tests:
- Rename _LinkedWorktreeMatchesMain → _LinkedWorktreeFallsBackToMain
  (the semantic is now fallback, not widening).
- Add _LinkedWorktreeWithOwnSessionIgnoresMain to lock in property 1.
- Add _LinkedFallbackSkipsSiblings: when neither linked nor main has a
  session, return empty rather than wandering into a sibling worktree's
  session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b6ccb16e9ce3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants