Skip to content

fix(worktrees): read the worktree id from .name as well as .worktree_id - #85

Open
thecodedrift wants to merge 2 commits into
mainfrom
fix/worktree-hook-payload
Open

fix(worktrees): read the worktree id from .name as well as .worktree_id#85
thecodedrift wants to merge 2 commits into
mainfrom
fix/worktree-hook-payload

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

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_id at 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 keys cwd, 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-af8378fb661401c98 and the agent completed its work inside it.

One related thing left alone deliberately: worktree-remove.sh reads .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.

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.
@thecodedrift thecodedrift added the skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) label Aug 6, 2026
@thecodedrift
thecodedrift marked this pull request as ready for review August 6, 2026 17:24
Copilot AI lite review requested due to automatic review settings August 6, 2026 17:24
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 3m 55s —— View job


Review: fix(worktrees): read the worktree id from .name as well as .worktree_id

  • Gather context (PR diff, changed file, hook contract, related docs)
  • Understand the request (@claude /review — provide review feedback, no implementation)
  • Review diff for correctness, security, performance, clarity
  • Post inline comments on relevant lines
  • Post final top-level assessment

Overall: looks good, low risk, no blocking issues. This is a tightly scoped fix (1 file, +8/-3) to .agents/skills/worktrees-pnpm/scripts/worktree-create.sh.

What it does: widens the worktree_id lookup from .worktree_id only to .worktree_id // .worktreeId // .name, updates the failure message to match, and documents in the contract comment that the id field is not stable across callers (background-agent delegation sends .name, not .worktree_id).

Correctness

  • The jq fallback chain is left-associative and behaves as intended: first non-null/non-false value wins, so a caller sending both .worktree_id and .name still gets .worktree_id (no precedence bug).
  • Whatever value is picked still flows through the existing -z empty check and the regex/.. safety check further down (lines 44–47), so a malformed or unexpected .name fails loudly with a clear error rather than being used unsafely as a path/ref component. Good defense in depth — the new fallback doesn't bypass the existing validation.
  • No other file in the repo references the old error string ("no .worktree_id on stdin"), so nothing else breaks from the message change.
  • Left one inline note: the risk that some other WorktreeCreate caller sends an unrelated .name while genuinely lacking both id fields is real but small-blast-radius (fails the regex check cleanly rather than corrupting state), and it's already flagged in the contract comment — just calling it out explicitly.

Clarity / docs

  • The updated contract comment is a real improvement — it explains why multiple fields are accepted, not just that they are, which is exactly what the previous confusing "no .worktree_id on stdin" failure was missing.
  • Minor nit (inline): the comment rewrite drops .base_path from the list of documented-but-unused stdin fields. Purely cosmetic, but worth a second look in case it was accidental.
  • Not required for this PR, but SKILL.md's verify snippet (.agents/skills/worktrees-pnpm/SKILL.md ~line 108) still only demonstrates probing with .worktree_id; a follow-up could show .name too so the fallback is discoverable without reading the script.

Scope

  • The PR explicitly leaves worktree-remove.sh's .worktree_path field alone, noting it fails open (stale directory, not lost work) rather than guessing at a fallback. That's a reasonable, well-justified boundary for this PR — agreed it shouldn't be bundled in here without a real captured payload to verify against.
  • skip-changeset label is appropriate — this is a dev-tooling hook script fix, not user-facing release content.

No security, performance, or test-adequacy concerns beyond what's noted above. Nice, evidence-based fix (captured from a real failing payload rather than inferred).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +21 to +26
# 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.
Comment on lines +20 to +21
# 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, ...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fallback chain looks correct (worktree_idworktreeIdname, 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR intentionally ships no release note (bypasses the changeset requirement)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants