fix(worktrees): read the worktree id from .name as well as .worktree_id - #85
fix(worktrees): read the worktree id from .name as well as .worktree_id#85thecodedrift wants to merge 2 commits into
Conversation
Delegating to a background agent with worktree isolation failed outright: "WorktreeCreate: no .worktree_id on stdin". The hook replaces the default creation logic, so a non-zero exit means no worktree and no agent. The payload for that path carries `.name` (e.g. "agent-a5e1de46e730bdfd7") and no `.worktree_id` at all — captured from a real invocation rather than inferred. The hook now reads `.worktree_id`, `.worktreeId`, or `.name`, and its contract comment records that the field is not stable across callers, since the failure message otherwise reads as though the harness sent nothing. Verified by re-running the delegation: the worktree was created and the agent completed its work in it.
|
@claude /review |
|
Claude finished @thecodedrift's task in 3m 55s —— View job Review: fix(worktrees): read the worktree id from
|
There was a problem hiding this comment.
Pull request overview
Fixes WorktreeCreate hook robustness by accepting multiple possible worktree identifier fields from stdin, preventing worktree-isolated background-agent delegation from failing when .worktree_id is absent.
Changes:
- Read the worktree identifier from
.worktree_id,.worktreeId, or.name. - Improve the failure message when no identifier is present.
- Expand the hook contract comment to document identifier-field drift across callers.
| # stdin - JSON identifying the worktree, plus .cwd, .session_id, ... | ||
| # The id field is NOT stable across callers: a background agent's | ||
| # payload carries `.name` (e.g. "agent-a5e1de46e730bdfd7") and no | ||
| # `.worktree_id` at all, so both are read. Accepting only one of them | ||
| # fails worktree creation outright with a message that reads like the | ||
| # harness sent nothing. |
| # Contract (docs: code.claude.com/docs/en/hooks): | ||
| # stdin - JSON with .worktree_id (and .base_path, .cwd, .session_id, ...) | ||
| # stdin - JSON identifying the worktree, plus .cwd, .session_id, ... |
There was a problem hiding this comment.
Minor nit: the previous comment listed .base_path among the "other" stdin fields (.base_path, .cwd, .session_id, ...); the rewritten line drops it (.cwd, .session_id, ...). Not used by the script either way, so purely cosmetic — but if it was dropped intentionally because .base_path turned out not to be part of the real contract, that's worth a half-sentence; otherwise it reads like an unintentional trim.
|
|
||
| payload=$(cat) | ||
| worktree_id=$(printf '%s' "$payload" | jq -r '.worktree_id // empty') | ||
| worktree_id=$(printf '%s' "$payload" | jq -r '.worktree_id // .worktreeId // .name // empty') |
There was a problem hiding this comment.
The fallback chain looks correct (worktree_id → worktreeId → name, each caught by the same downstream -z/regex validation), and priority order is right — a caller that sends both .worktree_id and .name still gets .worktree_id. One thing to flag for awareness rather than as a bug: .name is only justified by one captured background-agent payload (per the PR description), and the comment above already acknowledges the id field "is NOT stable across callers." If some other WorktreeCreate invocation ever sends an unrelated .name (e.g. a display label with spaces) while genuinely lacking .worktree_id/.worktreeId, this will now pick it up as the id — worst case it fails the regex check below with a clear error rather than silently corrupting anything, so the blast radius is small, but it's worth knowing this is a documented assumption, not a guaranteed contract.
The contract comment said "both are read" while the implementation accepts three (.worktree_id, .worktreeId, .name). Name all three, and write down that .name rests on one captured payload rather than a guaranteed contract — an unrelated .name lands on the regex guard below and fails loudly, which is the intended floor. Restore .base_path to the documented-but-unused field list (dropped in the rewrite, not deliberately), and show a .name payload in the SKILL.md verify probe so the fallback is discoverable without reading the script. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Delegating to a background agent with worktree isolation failed outright with
WorktreeCreate: no .worktree_id on stdin. Because this hook replaces Claude Code's default creation logic, a non-zero exit means no worktree is created and the agent never starts — so worktree-isolated delegation was simply unavailable.The payload for that path carries
.name(e.g.agent-a5e1de46e730bdfd7) and no.worktree_idat all. That was captured from a real invocation rather than inferred: the hook was temporarily made to dump stdin on failure, the delegation was re-run, and the saved JSON had keyscwd,hook_event_name,name,prompt_id,session_id,transcript_path.The hook now reads
.worktree_id,.worktreeId, or.name, and its contract comment records that the id field is not stable across callers — worth stating, because the previous failure message read as though the harness had sent nothing at all, which sends you looking in the wrong place.Verified by re-running the delegation against the fixed hook: the worktree was created at
worktrees/agent-af8378fb661401c98and the agent completed its work inside it.One related thing left alone deliberately:
worktree-remove.shreads.worktree_path, which may be subject to the same drift. It fails open — a missing field just leaves the worktree in place — so the symptom is a stale directory rather than lost work, and I did not want to guess at a fallback that could resolve to something removable. Worth confirming separately against a real remove payload.