feat(source-control): route /worktree create through a shared worktree-creation helper (#399 Phase A) - #898
Conversation
…l-root helper (#399 Phase A) Add scripts/worktree-create.sh as the single owner of worktree creation: external-root path computation <root>/<owner>-<repo>-<slug>, slug sanitization, worktree.baseRef (fresh/head) resolution with the default branch resolved symbolically, git worktree add, and a reimplementation of Claude Code's .worktreeinclude copy (the matched-AND-gitignored intersection, bypassed when a worktree is created with git directly). The flag CLI is the stable seam the Phase B WorktreeCreate hook will share. Add a new worktree_root userConfig directory key (mirroring babysit_worktree_root's shape). When it is unset the helper refuses with guidance (exit 3) rather than falling back to the in-repo .claude/worktrees/ default, whose nested placement triggers Claude Code's CLAUDE.md/rules double-load bug (#400, upstream anthropics/claude-code #29599 / #23565). Patch /worktree create (SKILL.md + context/create.md) to run the helper and then EnterWorktree(path:) into the created worktree, stopping on any non-zero helper exit and never falling back to EnterWorktree(name:). Document the expected out-of-.claude/worktrees approval prompt and the declined-approval recovery. Phase B (the native WorktreeCreate hook) stays gated on the two empirical upstream gates (#77566 stdin field set, #78212 out-of-tree permission behavior) and is not shipped here. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Address independent-review findings on the shared helper: - CRITICAL: a value-taking flag given as the last token (e.g. `--name` with no value) made `shift 2` fail on a single positional, leaving $# unchanged and spinning the arg loop forever. Guard each value-taking flag and exit 2. - Validate --name up front against the EnterWorktree schema (<=64 chars, each /-segment [A-Za-z0-9._-]) and refuse invalid names (exit 2) instead of letting `git worktree add` fail opaquely — the branch is used verbatim, so an unsafe name must fail loud here. - Parse owner/repo with a named function that handles 3+-segment remote paths: Azure DevOps `_git` and SSH `v3` markers are dropped, GitLab subgroups resolve to subgroup/repo. Previously assumed exactly two segments. - .worktreeinclude copy now checks mkdir/cp exit status, only counts real successes, reports failures, and exits 4 if any file failed (was: unconditional increment, silent failures). Enumerate NUL-delimited (`ls-files -z` + `read -d ''`) so paths with spaces/newlines are not dropped. - --base-ref fresh now warns on stderr when origin/HEAD is uncached and it falls back to local HEAD, rather than silently branching from HEAD. Tests added: trailing valueless flag (all four), unsafe/over-long name refusal, Azure + GitLab-subgroup URL parsing, valid-name slug transform + verbatim branch, uncached origin/HEAD fallback warning, --repo-dir outside a repo, trailing-slash root. 37 cases total, all passing. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 4m 39s —— View job PR Review
This is a well-designed, carefully-guarded piece of work. The output contract, refuse-with-guidance pattern, IMPORTANT —
|
|
Claude finished @kyle-sexton's task in 4m 33s —— View job Security Review
Scope: Overall posture: The script is carefully written. Variables are consistently quoted, the branch name is validated against a strict allowlist before it reaches any git call, NUL-delimited enumeration is used throughout the Two SUGGESTION-level issues found: SUGGESTION —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e7ba0986f2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…et exec bit CI hygiene lane flagged two things on the worktree-create scripts: - comment-hygiene bans tracker/issue references in code comments (TODO(#issue) is the only sanctioned form). Remove the issue-number references from the helper's and test's comments and user-facing help/refuse text; the rationale stays in prose. Traceability lives in the changelog, commit messages, and the PR — repo-internal issue numbers are noise to a plugin consumer anyway. - exec-bit requires tracked shebang scripts to be recorded 100755; both new scripts were committed 100644 (Windows core.filemode). Set the executable bit. No behavior change; 37 tests still pass, shellcheck clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 57s —— View job Security Review
Scope: Overall posture: The script is carefully written. Variables are consistently quoted throughout, SUGGESTION —
|
|
Claude finished @kyle-sexton's task in 4m 4s —— View job PR Review
What the latest commit (
|
#882 (source-control) merged into main at 0.16.0, which this branch's prior 0.16.1 was already one past — resolved the resulting plugin.json/CHANGELOG.md conflict, kept as-is. Full fresh collision sweep after the merge found source-control now carries TWO additional open PRs beyond #840: #895 (fix/548-babysit-worktree-head-safety, claims 0.15.10) and #898 (feat/399-shared-worktree-helper, claims 0.17.0 — a minor bump above this branch's prior 0.16.1). Re-bumped source-control to 0.17.1 to stay ahead of all three (#898's 0.17.0, #895's and #840's 0.15.10). claude-ops (0.17.5, no collision), work-items (0.20.1, held behind #861's 0.20.0), repo-hygiene (0.4.6, no collision), and guardrails (0.9.6, no collision) re-verified against current main and all live open PRs — unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1V3gkrfSf75isB8MiDy3o
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b2f5377af1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…th routing Eval #1 (create-routing-enterworktree-last) still asserted the old EnterWorktree(name: ...) final action. The create action now routes through the shared worktree-create.sh helper and calls EnterWorktree(path: ...) on the printed path. Update the expected_output prose and add an expectation locking the helper-routing / path: invariant so the eval no longer rewards the in-repo name: path this change removes.
|
Claude finished @kyle-sexton's task in 4m 9s —— View job PR Review
|
|
Claude finished @kyle-sexton's task in 2m 56s —— View job Security Review
Scope: Overall posture: The latest commit ( IMPORTANT —
|
|
Automated babysit-lane classification (safe tier — reply only; not resolving threads). Re: Claude PR review (comment 5038417106, head
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2ad0030a07
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Automated babysit-lane classification (safe tier — reply only; not resolving threads). Re: Claude security review (comment 5038416880, head Note: this review's own text states it found no high-severity items at head — the higher-severity words in its body are that posture prose, not live findings. Two genuine suggestion-level items:
|
|
Automated babysit-lane classification (safe tier — reply only; not resolving threads). Re: superseded prior-commit reviews — Claude PR review 5038353117 and Claude security review 5038353867, both reviewed at the earlier commit Both were re-run at the current head
|
|
Automated babysit-lane classification (safe tier — reply only; not resolving threads). Re: the re-reviews at head Eval fixture (Codex P2): confirmed CLOSED — the PR review marks it "Addressed — Eval fixture ✓" (fixed in The rest are the same carried items classified on the
Net: one IMPORTANT ( |
|
This was generated by AI (decide lane — decision-brief pass; parked PR-thread decisions surfaced via periodic scan). Two parked design decisions in this PR's Codex review threads have no ratified resolution. Briefing both. Base-ref facts below are verified against the primary source (Claude Code worktrees doc, Decision 1 — helper reads
|
… config worktree.baseRef is a Claude Code settings.json key (governing native EnterWorktree/--worktree), not a git config key. The helper was probing `git config worktree.baseRef`, which never holds that value, so a user who set baseRef=head silently got the fresh path in the new worktree. Remove the probe; the helper now defaults to fresh and takes --base-ref from the caller. The skill reads the effective Claude setting (project settings.json over user) and passes --base-ref head when set. Fix the same wrong claim in the helper --help text and the create.md "Base branch" prose. Not data loss: unpushed commits stay on the branch in the main checkout; the new worktree just does not start from them. The create-eval alignment for this routing change is already handled on the branch (test: align worktree create eval with helper-based path routing). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 37s —— View job PR Review
What
|
|
Claude finished @kyle-sexton's task in 2m 42s —— View job Security Review
Scope: Overall posture: The helper is carefully written. Variables are consistently quoted throughout, SUGGESTION —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa0f5e8f6a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The helper's core purpose is keeping worktrees OUT of the source checkout; an in-repo worktree reintroduces the CLAUDE.md/rules double-load bug. The refuse guard only rejected an unconfigured or placeholder root — a root explicitly pointed inside the repo (e.g. the old .claude/worktrees/ location) still built a nested target and `git worktree add` happily succeeded there, silently reintroducing the exact placement the helper exists to eliminate. Ask git whether the target sits inside this repo's working tree: walk up to the nearest existing ancestor of the computed target and compare its top level to ours. Both come from `rev-parse --show-toplevel`, so the check is immune to the path-format differences (drive-letter spelling, symlinks) that defeat a raw string prefix test. On a match, refuse with exit 3 and the same guidance shape as the unconfigured-root path; otherwise proceed unchanged. Verified: in-repo roots (existing, nonexistent-leaf, deep, ==toplevel) refuse with exit 3; external and shared-prefix-sibling roots are not falsely rejected; the unconfigured-root refuse still fires. shellcheck clean. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69e1c53fe1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Add a regression case for the new exit-3 guard: a root configured inside the repo (the old .claude/worktrees/ location) must refuse rather than build a nested target. Asserts exit 3 and that the message names the repository. Suite: 39 pass, 0 fail; shellcheck clean. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…guard
On a Windows shell `\` is a path separator `git worktree add` resolves, but the
containment guard cannot see through it: the ancestor walk splits on `/`, so an
all-backslash `${probe%/*}` never changes, `parent == probe` breaks the loop on
its first iteration, and the entire guard block is skipped. A backslash `--root`
therefore sailed through and git created the checkout inside the repo — even
without any `..`: a root pointing straight into the working tree or into `.git`
also failed open (exit 0, checkout inside the repo/.git).
Swap `\`→`/` on the root up front (before the append, anchor, normalize_path,
and walk) so a backslash root flows through the SAME code path as its
forward-slash form and the existing guards fire. Gated to Windows shells via
$OSTYPE — off-Windows `\` is a legal filename byte and must be left untouched.
cygpath is intentionally not used: it resolves relative paths against the CWD
(not $toplevel) and rewrites MSYS `/tmp` paths, both of which diverge from this
helper's contract; a pure separator swap defers all resolution to the existing
machinery. normalize_path is unchanged (still forward-slash grammar), so the
swap lands one grammar before it runs.
Verified on Git for Windows 2.54.0: backslash roots into the working tree, into
`.git`, and with `..` after a nonexistent component all now refuse (exit 3) with
nothing created; a genuine external backslash root still creates (exit 0) and
prints the forward-slash path EnterWorktree(path:) expects. Repo suite 60 pass /
0 fail (Windows; the backslash cases skip off-Windows); shellcheck clean; added
code shfmt-clean. Residual left out of scope: a drive-relative `C:foo` root (no
separator, resolved against the drive's CWD) — pathological for a worktree_root
and only fixable via cygpath-style CWD resolution.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # plugins/source-control/.claude-plugin/plugin.json # plugins/source-control/CHANGELOG.md
|
Claude finished @kyle-sexton's task in 4m 31s —— View job PR Review
What the latest commits doThree commits since the last full automated review pass (
|
| Input form | root | Behaviour |
|---|---|---|
Absolute POSIX /… |
"/" |
correct |
Windows drive C:/… |
"C:/" |
correct |
| Relative (anchored before call) | "" |
safe — anchoring ensures this only happens for C:foo (drive-relative, documented OOS) |
.. clamped at root (/a/../..) |
"/" |
pops a, no-op second pop → / |
The empty-array printf '%s%s' "$root" "${out[*]}" at the POSIX root is correct: "${out[*]}" with IFS='/' and an empty array expands to "", so the result is /. A worktree_path that normalizes to / would then have the ancestor walk probe / (a non-git directory on any real system), produce no location, and fall through to git worktree add which would fail to create //owner-repo-slug with an environment error (exit 4). Acceptable behaviour for a completely degenerate input.
The local IFS='/' scoping before printf is correct — it only affects "${out[*]}" expansion within that function scope.
Addressed — prior SUGGESTION: --root relative-path CWD ambiguity ✓
The b9ddad9 + f723710 combo fully closes this: relative roots are anchored to $toplevel before normalize_path and the containment walk, so both the existence check and git worktree add resolve against the same base. The ..-after-nonexistent variant is also closed.
Addressed — Windows backslash bypass ✓ (ac00bf4)
The canonicalization is correctly scoped to $root only (not $name or $repo_dir), correctly gated to Windows shells, and correctly applied before anchor + normalize + walk, so all three subsequent containment layers see a forward-slash path. The non-regression test (external backslash root still creates exit 0) confirms the guard doesn't over-reject legitimate external Windows paths.
Addressed — merge-from-main is clean ✓
The conflict was in plugin.json and CHANGELOG.md only. The worktree_root key (line 152) is correctly preserved between the pre-existing babysit_worktree_root and worktree_stale_days entries. The two new keys from main (babysit_extra_dependency_manager_logins, branch_issue_pattern) are clean additions with no structural conflict.
All IMPORTANT findings from the full review cycle: resolved ✓
| Finding | Commit |
|---|---|
.worktreeinclude intersection semantics |
Correctly classified INCORRECT — git ls-files -o -i without --exclude-standard does not load the standard gitignore chain; the implementation is correct |
worktree.baseRef reads from wrong store |
aa0f5e8 — dead git-config probe removed; caller owns policy via --base-ref |
Local settings.local.json override omitted |
7ce20d7 — three-tier precedence documented in create.md |
Relative-root CWD-vs-$toplevel ambiguity |
b9ddad9 + f723710 |
| In-repo placement bypass (source repo) | 69e1c53 |
| In-repo placement bypass (any checkout) | 96826cf |
Placement in .git dirs / bare clones |
81627ac |
..-after-nonexistent bypass |
f723710 |
| Windows backslash bypass | ac00bf4 |
Deferred items (all carried from prior review cycles, tracked in #657)
| Finding | Status |
|---|---|
--root omitted exits 3 (refuse), not 2 (usage) |
Deferred — guidance still actionable; cosmetic exit-code distinction |
Azure SSH git@ssh.dev.azure.com:v3/… URL not tested |
Deferred — gap on an already-working code path; no defect |
cp -p follows symlinks to out-of-repo targets (lines 322–325) |
Deferred (SUGGESTION, local-control only, no remote vector). Fix: [[ -L "$src" ]] && continue before the -f test, or a note in create.md. Tracked in #657 |
| Refuse message references nonexistent "worktree setup skill" | Deferred — /plugin configure is offered alongside and works; tracked in #657 |
git check-ref-format --branch not run after regex |
Deferred — names like feat/foo..bar pass the char-class regex but git rejects them (exit 4, not 2); tracked in #657 |
Drive-relative C:foo root (no / after :) |
Out-of-scope — cygpath CWD dependency makes a clean fix non-trivial; documented OOS in ac00bf4 comment |
Positives
normalize_pathdesign is correct: pure-string lexical collapse means it resolves..even through nonexistent components (whererealpath -mwould be needed but is GNU-only, andrealpath --/readlink -frequire existing prefixes). The no-op guarantee for ordinary paths (no./..///) is accurate — re-split and re-join with the same separator is idempotent.- Windows backslash handling is correctly scoped, gated, and sequenced: swap → anchor → normalize → walk is the right order. Using intermediate
bslash/fwdvariables avoids bash backslash-quoting pitfalls in parameter substitution. - Test suite is now at 60 pass / 0 fail (per the
ac00bf4commit message). Every containment bypass that was filed and fixed has a matching regression test. - Merge conflict resolution on
plugin.jsonis clean — the PR'sworktree_rootentry survived intact. - All prior positives carry:
need_valueguard,origin/HEADsymbolic resolution, refuse-with-guidance message, NUL-delimited.worktreeincludeenumeration, output contract,parse_owner_repofallback chain, eval fixture alignment, and theworktree.baseRefsettings.json fix.
Summary: The two commits since the last automated review (f723710, ac00bf4) are both correctly implemented and close the last known containment-guard bypass paths. The merge from main is clean. No IMPORTANT findings remain open. The deferred minors and SUGGESTION-level items are all non-blocking and tracked in #657. This PR is ready to merge.
|
Claude finished @kyle-sexton's task in 3m 12s —— View job Security Review
Scope: New commits since last security review ( Overall posture: The helper is carefully written. Variables are consistently double-quoted throughout; Resolved since last review ✓
SUGGESTION (carried) —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ff6d696be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Re: Anchor colon-bearing relative roots (create.md drive-relative |
Double-quoting the dotted `${user_config.worktree_root}` token made
Bash fail with `bad substitution` when the key is unset, so the
first-run/unconfigured user hit a shell error instead of the helper's
documented exit-3 refusal + guidance. Single-quote it so an unset value
reaches the helper as an inert literal — mirrors the #950 fix at the
parse-branch-issue call site. Addresses the create.md:70 review finding.
|
Re: Quote unset userConfig placeholders safely (create.md:70) — fixed in |
|
Claude finished @kyle-sexton's task in 1m 48s —— View job PR Review
What
|
| Finding | Commit |
|---|---|
.worktreeinclude intersection semantics |
CORRECTLY CLASSIFIED INCORRECT — git ls-files -o -i without --exclude-standard does not load the standard gitignore chain; the implementation is correct; the suggested --no-standard-excludes flag does not exist in git |
worktree.baseRef reads from wrong store |
aa0f5e8 — dead git-config probe removed; caller owns policy via --base-ref |
Local settings.local.json override omitted |
7ce20d7 — three-tier precedence documented in create.md |
Relative-root CWD-vs-$toplevel ambiguity |
b9ddad9 + f723710 |
| In-repo placement bypass (source repo) | 69e1c53 |
| In-repo placement bypass (any checkout) | 96826cf |
Placement in .git dirs / bare clones |
81627ac |
..-after-nonexistent bypass |
f723710 |
| Windows backslash separator bypass | ac00bf4 |
Unset userConfig placeholder bad substitution |
9f93c77 |
Deferred items (carried, all non-blocking, all tracked in #657)
| Finding | Status |
|---|---|
--root omitted exits 3 (refuse) not 2 (usage) |
Deferred — guidance is still actionable; cosmetic exit-code distinction |
Azure SSH git@ssh.dev.azure.com:v3/… not tested |
Deferred — gap on an already-working code path; no defect |
cp -p follows symlinks to out-of-repo targets |
Deferred (SUGGESTION, local-control only, no remote vector). [[ -L "$src" ]] && continue before line 388 closes it; a note in create.md is the minimum |
| Refuse message references nonexistent "worktree setup skill" | Deferred — /plugin configure works; tracked in #657 |
git check-ref-format --branch not run after regex |
Deferred — names like feat/foo..bar exit 4 instead of 2; tracked in #657 |
Drive-relative C:foo root (no / after :) |
Documented OOS in ac00bf4; tracked in #962 |
Positives
- Minimal, precisely-targeted fix — one character change, no collateral modifications.
- Symmetry with the prior fix at the
parse-branch-issuecall site — same class of problem (${user_config.dotted.key}in bash), same solution (single-quote), established pattern in the codebase. - Guard in the helper validates this end-to-end — the fix is not relying on undocumented Claude Code behavior; the detection at
worktree-create.sh:116explicitly matches*'${user_config'*(and theSC2016disable confirms the literal match is intentional), so the refuse-with-guidance path is robustly exercised. - All prior positives carry:
need_valueguard,origin/HEADsymbolic resolution, refuse-with-guidance message, NUL-delimited.worktreeincludeenumeration, output contract,parse_owner_repofallback chain, eval fixture alignment, all containment guards, and theworktree.baseRefsettings.json fix.
Summary: 9f93c77 closes the last open Codex P2 — the bad substitution failure on the first-run path that prevented the documented exit-3 guidance from ever being shown. No new findings. All IMPORTANT and P2 findings from the full review cycle are resolved. Remaining deferred items are SUGGESTION-level or minor test gaps, tracked in #657. This PR is ready to merge.
|
Claude finished @kyle-sexton's task in 3m 46s —— View job Security Review
Scope: New since last security review ( Overall posture: No CRITICAL or IMPORTANT findings at HEAD. The fix in Resolved since last review ✓Codex P2 — unset SUGGESTION —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f93c7706e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Second call site of the same fix as the prior create.md commit — the
top-level SKILL invariant also double-quoted the dotted
`${user_config.worktree_root}` token, so an unset key still aborted with
Bash `bad substitution` instead of the helper's exit-3 guidance.
Single-quote it to reach the helper as an inert literal.
|
Re: Single-quote the worktree_root placeholder (SKILL.md:69) — fixed in |
|
Claude finished @kyle-sexton's task in 2m 22s —— View job PR Review
What
|
| Finding | Resolution |
|---|---|
.worktreeinclude intersection semantics |
CORRECTLY CLASSIFIED INCORRECT — git ls-files -o -i without --exclude-standard does not load the standard gitignore chain; the implementation is correct; the suggested --no-standard-excludes flag does not exist in git |
worktree.baseRef reads from wrong store |
aa0f5e8 — dead git-config probe removed; caller owns policy via --base-ref |
Local settings.local.json override omitted |
7ce20d7 — three-tier precedence documented in create.md |
Relative-root CWD-vs-$toplevel ambiguity |
b9ddad9 + f723710 |
| In-repo placement bypass (source repo) | 69e1c53 |
| In-repo placement bypass (any checkout) | 96826cf |
Placement in .git dirs / bare clones |
81627ac |
..-after-nonexistent bypass |
f723710 |
| Windows backslash separator bypass | ac00bf4 |
Unset userConfig placeholder bad substitution (create.md) |
9f93c77 |
Unset userConfig placeholder bad substitution (SKILL.md) |
a90cc41 ← this commit |
Deferred items (all carried, all non-blocking, tracked in #657)
| Finding | Status |
|---|---|
--root omitted exits 3 (refuse) not 2 (usage) |
Deferred — guidance is still actionable; cosmetic exit-code distinction |
Azure SSH git@ssh.dev.azure.com:v3/… not tested |
Deferred — gap on an already-working code path; no defect |
cp -p follows symlinks to out-of-repo targets (worktree-create.sh:387-390) |
Deferred (SUGGESTION, local-control only, no remote vector). [[ -L "$src" ]] && continue before the -f test closes it cleanly; so would a note in create.md. Neither is blocking. |
| Refuse message references nonexistent "worktree setup skill" | Deferred — /plugin configure is offered alongside and works; tracked in #657 |
git check-ref-format --branch not run after regex |
Deferred — names like feat/foo..bar exit 4 instead of 2; tracked in #657 |
Drive-relative C:foo root (no / after :) |
Documented OOS; tracked in #962 |
Positives
- Minimal, precisely-targeted fix — one character change, zero collateral modifications.
- Symmetry with the prior fix.
9f93c77fixedcreate.md:70;a90cc41applies the identical fix toSKILL.md:69— same class of problem (dotted${user_config.*}placeholder in bash), same solution (single-quote), consistent pattern. - The test at
worktree-create.test.sh:74already covers this code path — the unexpanded-token case passes'${user_config.worktree_root}'directly to the helper and asserts exit 3, confirming the guard fires for both thecreate.mdandSKILL.mdforms. - Both call sites are now consistent. A model fast-pathing through the
SKILL.mdinvariant block and a model following the fullcreate.mdprocedure both produce the same shell command for the unset-key case. - All prior positives carry:
need_valueguard,origin/HEADsymbolic resolution, refuse-with-guidance message, NUL-delimited.worktreeincludeenumeration, output contract,parse_owner_repofallback chain, eval fixture alignment, all containment guards, and theworktree.baseRefsettings.json fix.
Summary: a90cc41 applies the final single-character fix that closes the last Codex P2 — the SKILL.md headline invariant now single-quotes the worktree_root placeholder, matching create.md:70. No new findings. All IMPORTANT and P2 findings from the full review cycle are resolved. Remaining deferred items are SUGGESTION-level or minor test gaps, tracked in #657. This PR is ready to merge.
|
Claude finished @kyle-sexton's task in 2m 36s —— View job Security Review
Scope: New since last security review ( Overall posture: No CRITICAL or IMPORTANT findings at HEAD. The Resolved since last review ✓SKILL.md unset-placeholder SUGGESTION (carried) —
|
| Finding | Commit |
|---|---|
.worktreeinclude intersection semantics |
CORRECTLY CLASSIFIED INCORRECT — git ls-files -o -i without --exclude-standard does NOT load the standard gitignore chain; only --exclude-from patterns are active; check-ignore -q computes the real intersection |
worktree.baseRef reads from wrong store |
aa0f5e8 — dead git config worktree.baseRef probe removed; caller owns policy via --base-ref |
Local settings.local.json override omitted |
7ce20d7 — three-tier precedence documented in create.md |
Relative-root CWD-vs-$toplevel ambiguity |
b9ddad9 + f723710 |
| In-repo placement bypass (source repo) | 69e1c53 |
| In-repo placement bypass (any checkout) | 96826cf |
Placement in .git dirs / bare clones |
81627ac |
..-after-nonexistent bypass |
f723710 |
| Windows backslash separator bypass | ac00bf4 |
Unset userConfig placeholder bad substitution (create.md) |
9f93c77 |
Unset userConfig placeholder bad substitution (SKILL.md) |
a90cc41 |
Notes (informational, not security findings)
.worktreeincludeintersection semantics are correct.git ls-files -o -iwithout--exclude-standarddoes NOT load the standard gitignore chain. Only--exclude-from=.worktreeincludepatterns are active; thecheck-ignore -qpost-filter computes the genuine gitignore intersection. The earlier IMPORTANT finding was incorrect;--no-standard-excludesis not a valid git flag.- URL-derived path components are traversal-safe.
parse_owner_repojoins segments with-(not/), so a crafted remote URL (git@host:../../etc/passwd.git) producesetc-passwd-<slug>inside$root— a literal filename component, never a traversal above it. git ls-filesoutput is bounded to the repository tree.relvalues fromgit ls-files -o -i -zare repository-relative;../traversal paths are not emitted through standard git behavior...segments in names are not a traversal vector. The name regex permits.chars, but the slug step converts/→-, making../..→..-..-slug(a literal component inside$root). Git then rejects..as an invalid branch name component (exit 4).IFS='/' read -r -a partsinparse_owner_repois command-scoped. TheIFS='/'prefix applies only toread; it does not permanently modify IFS.$(dirname "$dest")cannot be injected.$destis formed via double-quoted variable expansion; any special characters in$relare literal string content passed to the externaldirnamecommand, not re-evaluated by the shell.--is-inside-git-diroutput comparison is exact-match safe.[[ "$(…)" == "true" ]]uses a quoted right-hand side in[[…]], so no glob expansion is possible. Git outputs only"true","false", or nothing on error; the last case evaluates"" == "true"→ false.- No GitHub Actions changes in this PR — no workflow injection surface to review.
Summary: No CRITICAL or IMPORTANT findings at HEAD a90cc41. The latest commit closes the last identified call site of the unset-userConfig-placeholder bad substitution issue. The sole remaining open security item is the cp -p symlink traversal (SUGGESTION, local-control only, deferred throughout the review cycle).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a90cc41140
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Re: Escape worktree_root before invoking the helper (single-quote breaks on a value containing The interim single-quote fix on this PR is a net improvement: it correctly handles the COMMON unset/first-run path (literal token → helper exit-3 guidance), trading it for a RARE special-char-in-path case. Neither quote style is safe for an arbitrary path value under raw template substitution — the complete fix needs the Claude Code Deferring per the loop-breaker carve-out: both failure modes fail LOUD (visible Bash |
…1333) *This was generated by AI during an autonomous work-loop execution session.* Closes #1016 ## Summary #1016 batched two P2 follow-ups deferred from PR #898's review. **Only one of them is still live** — the other landed inside #898 itself, which the reproduction below establishes before any code was changed. ### Site 2 — git ref-syntax validation (the actual fix) `worktree-create.sh` validates `--name` against the `EnterWorktree` schema (a character class of letters, digits, dots, underscores, and dashes per `/`-separated segment, plus a 64-char cap) and documents that a name failing validation is refused with **usage exit 2**. A header comment asserted that class was "a strict subset of what git refs allow, so a validated name is always a creatable branch." That assertion is false. These names satisfy the class yet git rejects them as refs: | Name | Why git rejects it | |---|---| | `feat/foo..bar` | contains `..` | | `feat/.`, `.foo` | component starts/ends with `.` | | `foo.` | ends with `.` | | `foo.lock`, `feat/x.lock` | reserved `.lock` suffix | | `HEAD` | reserved name | | `-lead` | leading dash | Reproduced against a throwaway fixture repo on the pre-fix helper — every one of them fell through to `git worktree add` and returned **environment exit 4**, the code reserved for "not a git repo, or `git worktree add` failed". Since the caller's correction flow keys on exit 2, an invalid name was indistinguishable from a broken environment. **Fix:** follow the schema check with `git check-ref-format --branch`, per the disposition's own fix direction. Three details are load-bearing: - **Both streams are discarded.** On success `--branch` *echoes the name to stdout* — leaving it would corrupt the helper's "created worktree path is the SOLE stdout line" output contract, which `/worktree create` parses to feed `EnterWorktree(path:)`. A regression test asserts stdout is still exactly the path. - **No repository is required.** Git documents `--branch` as repository-scoped because of its `@{-n}` previous-branch expansion; `@`, `{`, and `}` are already outside the character class, so that path is unreachable and the check is correct run from anywhere. Verified empirically outside any repo. - **Option-shaped names are safe.** `git check-ref-format --branch --help` (and `-h`, `--normalize`, `--branch`) exit 128 with "not a valid branch name" — git parses no further options after `--branch`, so no name is mistaken for a flag and nothing opens a pager. **No over-rejection.** Ran a matrix over 24 names with a *fresh fixture repo per name* (an earlier shared-repo matrix produced false discrepancies from cross-case contamination): for every name the character class admits, `check-ref-format --branch` and `git worktree add -b` agree exactly. `feat/-lead`, `foo-`, `head`, `_x`, `a`, and `feat/scope.v2_final-1` all still create. The false "strict subset" comment is corrected, and the exit-code header, `--help` text, and `skills/worktree/context/create.md`'s name-validation list are brought in line. Those two files are the only tracked surfaces restating the rules — checked, not assumed. ### Site 1 — bare-repo root detection (no code change; already fixed) Per the Bug Investigation Rule this was reproduced first, and **it does not reproduce**. A root under a bare clone exits 3 today with "worktree target is inside a git directory". Git history explains why: `81627ac` ("reject worktree roots inside a git directory") landed *inside PR #898*, after the bare-repo thread was dispositioned. It added the `--is-inside-git-dir` probe, which returns `true` for both a `.git` directory and a bare repository — exactly the fix direction #1016 asks for. That commit's own resolution comment says so: *"This closes the `.git`-directory case here and, via the same `--is-inside-git-dir` mechanism, the sibling bare-clone gap deferred to #657 — #657 can drop the bare-repo item."* The #657 → #1016 batch conversion carried the stale line forward anyway. `worktree-create.test.sh` already covers it (`root inside a bare clone refuses exit 3`), so there is no coverage gap to close either. Nothing to do beyond recording the finding. ### Note for reviewers The `--name %q sanitizes to an empty slug` guard is now **unreachable**: an empty slug requires a name of only `-` and `/`, which must start with `/` (fails the character class) or `-` (fails `check-ref-format`). Left in place deliberately — it is cheap defense-in-depth whose reachability depends on check ordering, and removing it is outside this item's scope. ## Test plan - `plugins/source-control/scripts/worktree-create.test.sh` — **81 assertions pass, 0 fail** (was 60). Because this script is load-bearing for every worktree the work-loop provisions, the whole suite was run, not just the new cases: normal creation, slug/path computation, base-ref `fresh`/`head`, `.worktreeinclude` copying, and all existing containment guards are unchanged and green. - New cases: 8 git-invalid names each asserted **exit 2, not 4**, with the message naming the branch grammar; plus a valid-name case asserting creation still succeeds *and* that stdout remains the sole path line. - Pre-fix reproduction and post-fix verification run through the same harness against both helper revisions. - `shellcheck` clean on both scripts (repo `.shellcheckrc`). - `scripts/check-changelog-parity.sh --check` and `--check-bump origin/main` pass (version `0.26.3` → `0.26.4` with a matching entry). - Full suite, shellcheck, both changelog-parity gates, skill-portability, and markdownlint were **re-run after merging main in** — all green on the merged head (78/78 assertions). - `scripts/check-skill-portability.sh origin/main`, `scripts/check-silent-skips.sh`, and `markdownlint-cli2` on both changed markdown files pass. ### Review round 1 — CWD discovery regression (fixed in `a286444`) Codex caught a regression the ref check itself introduced, and it was the **real-world shape**, not a corner case. `git check-ref-format --branch` takes a branchname-shorthand and so performs **repository discovery**. Run unscoped it inherited the caller's CWD, so from a directory whose `.git` names a gitdir that no longer exists — a stale checkout, exactly what this plugin's own worktree cleanup handles — git exits 128 and a **valid** name was rejected with exit 2, creating nothing. The documented invocation in `SKILL.md` / `context/create.md` omits `--repo-dir`, so the CWD *is* the default. Reproduced end-to-end before fixing: identical invocation exited 2 from the stale dir and 0 from a neutral one. Fixed by scoping the check — with a refinement on the suggestion: `-C "$repo_dir"` alone would still misreport when `repo_dir` is the broken default, so the grammar check now runs **after** the exit-4 repository probe and uses `-C "$toplevel"`. A healthy repository is then guaranteed, so a non-zero exit can only mean the name. **Ordering consequence, stated plainly:** exits 3 (root unconfigured) and 4 (not a repository) can now precede an invalid-name exit 2. This matches the file's existing shape rather than introducing a new one — the pre-existing `--base-ref` and empty-slug exit-2 checks already sat after both. The character-class and length checks still run first, before any git call. `--help`, the exit-code header, and `create.md` say so. Two regression tests added: a valid name from a stale-`.git` CWD exits 0 and prints the path; an invalid name from that same CWD still exits 2. Suite is now **81/81**. ### Conflict resolution (2026-07-25) main bumped `source-control` to `0.26.3` while this PR was open. Both sides landed the **same version literal**, so git auto-merged `plugin.json` to a single `0.26.3` with **no conflict reported** — a silent collision that would have shipped two different releases under one version. Caught by re-reading the merged file; this branch is retargeted to **`0.26.4`**, with main's `0.26.3` CHANGELOG section preserved verbatim below this PR's entry. Integrated as a **merge commit** rather than a rebase. The rebase was done and verified first, but publishing it needed a force-push that this environment declines, so the content-equivalent merge was used instead — the merged tree is **byte-identical** to the verified rebased tree. Squash merge collapses the merge commit at land time. No semantic overlap with main's incoming `worktree` changes: those add external-root convention prose and touch neither name validation nor the exit-code contract. ## Related - Follow-up batch from PR #898 review threads (codex P2, dispositioned 2026-07-22). - `81627ac` — the commit that already closed Site 1. - Folded from the #657 sweep line. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and special-char values (#1334) *This was generated by AI during work-loop execution.* ## Summary - `${user_config.worktree_root}` substitution into skill content is **raw text substitution, not shell-escaped** (confirmed this session against the official [plugins-reference § User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration) docs), so neither quote style around the inline `--root '${user_config.worktree_root}'` literal in `context/create.md` / `SKILL.md` was fully safe: unset broke double-quoting (original finding), and a configured root containing `'`, `$`, or a backtick broke the interim single-quoted fix (#898). - `worktree-create.sh` gains an additive `--root-file <path>` flag that reads the root from a file's first line instead of a `--root` process argument. Both render sites now write the substituted value to a temp file via a quoted heredoc (`<<'WT_ROOT_EOF'` — fully literal, no expansion or quote-processing) and pass `--root-file`, never inlining the value in a `--root` shell literal. - The existing unset/unexpanded-token refuse (exit 3, guidance on stderr, no `EnterWorktree(name:)` fallback) is reused unchanged and reached through the file path: an unset key still leaves the literal `${user_config.worktree_root}` token, the heredoc writes it verbatim, and the guard still fires. - `--root` is untouched and stays available for a caller that already holds the value as a real process argument (a hook, or direct CLI use) — no shell-literal risk on that path. ## Test plan - Extended `plugins/source-control/scripts/worktree-create.test.sh` with 8 new cases covering the `--root-file` flag: mutual exclusivity with `--root` (exit 2), a missing file (exit 2), a root containing `'`, `$`, and a backtick materializing at the exact computed path (exit 0), empty file content refusing (exit 3, reuses the unset guard), and the literal unexpanded `${user_config.worktree_root}` token via the file refusing (exit 3). - Ran the full suite locally: **68/68 pass**, exit 0. - Added a create-action eval (`plugins/source-control/skills/worktree/evals/evals.json`, id 9) asserting the skill renders the safe out-of-band handoff (no single-quoted `${user_config.worktree_root}` shell literal) and preserves the exit-3 stop-and-surface contract. - `shellcheck` clean on both the helper and its test file (after moving one `# shellcheck disable=SC2016` to the line it actually needed to cover). - `markdownlint-cli2` clean on the two render-site markdown files after fixing an MD031 (blanks-around-fences) violation introduced by an early draft. - Validated `plugin.json` and the extended `evals.json` against their respective JSON Schemas (`check-jsonschema`, both `ok`). - Ran the repo's local quality gates against `origin/main` for this diff: docs-only detector (not docs-only, correctly runs full suite), `check-changed-skills.sh` (worktree skill: PASS, 0 errors), `check-changelog-parity.sh --check` and `--check-bump` (both pass — version bump 0.26.2 → 0.26.3 with a matching `## [0.26.3]` entry), `check-skill-portability.sh` (no unexcused coupling tokens). - Debugged and fixed one test-authoring pitfall along the way: the special-char fixture root initially inherited `$TEST_TMPDIR`'s bare POSIX `/tmp/...` form, which defeated MSYS's POSIX→Windows argv auto-conversion for `git.exe` when the path also carried `$`/backtick bytes, silently landing the worktree at the wrong physical location even though the printed path and exit code looked correct. Fixed by anchoring that one fixture to a `cygpath -m`-normalized (`TEST_TMPDIR_NATIVE`) drive-letter path instead — matching how a real Windows `directory`-type `userConfig` value actually resolves. ## Related Closes #965. Origin: #898 (interim single-quote fix) and the Codex review that surfaced the special-char gap; consolidated triage/ratification trail is on the issue itself. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ult remote (#1730) Closes #904 ## Summary `worktree-create.sh --base-ref fresh` resolved the base ref by probing `refs/remotes/origin/HEAD` and nothing else. A repository cloned with `git clone -o upstream` has no `origin` at all, so `fresh` took the remoteless local-`HEAD` fallback even though `upstream/HEAD` was correctly cached — and a worktree created from a feature branch carried unpushed local commits into a base that `fresh` promises is the remote default branch. ## Fix `fresh` now resolves the effective default **remote** before probing any symref, through the three-rung chain the issue prescribes: 1. the current branch's configured remote (`branch.<name>.remote`) 2. `origin`, when it exists 3. the sole remote, when the repository has exactly one The resolved remote's `HEAD` symref supplies the base. Nothing hardcodes a default branch name — resolution stays symbolic, as the portability lint requires. Details worth a reviewer's attention: - **Rung 1 rejects two values.** A configured remote is accepted only when it names a remote that still exists, so stale config cannot shadow a healthy `origin`; and git's `.` sentinel (meaning "tracks a local branch", not a remote — `refs/remotes/./HEAD` is nonsense) is refused. A detached `HEAD` has no branch, so the rung is skipped rather than erroring. - **The HEAD probe deliberately does not cascade back down the rungs.** `fresh` means the *effective* remote's default branch; quietly substituting a different remote's default branch is a worse failure than the fallback, because the caller cannot see it happen. - **The `HEAD` fallback and its loud warning remain** for the genuinely unresolvable cases, and the warning now names the cause: the resolved remote whose `HEAD` is uncached (with the `git remote set-head <remote> --auto` fix), or the absence of any default remote. - **Every git read in the resolver is `tr -d '\r'`-trimmed.** Under `git.exe` on an MSYS/Cygwin shell the output carries CRLF, and an untrimmed `upstream\r` would make each downstream lookup miss while still reading correctly in an error message. - **Behavior change beyond the non-`origin`-clone case:** rung 1 also applies in repos that *do* have `origin`. When `branch.<name>.remote` names a different existing remote, `fresh` now bases on that remote's default branch. That is the prescribed precedence; it is called out in the CHANGELOG so it is not a surprise. This closes a gap the helper shared with Claude Code's own native `fresh`, which [keeps `origin/HEAD` current](https://code.claude.com/docs/en/worktrees#choose-the-base-branch) and falls back to local `HEAD` when `origin/HEAD` is absent (fetched and verified while preparing this change). The helper is now deliberately more general than the native behavior in remote resolution — though not a strict superset, since it reads the cached ref rather than fetching; `create.md` says so explicitly. ## Test plan `plugins/source-control/scripts/worktree-create.test.sh` — **128 assertions, exit 0**, no failures or skips. The fixture gained `--remote-name` plus helpers that seed a second remote at a distinguishable tip. New coverage: a sole non-`origin` remote; the uncached-`<remote>/HEAD` warning naming the resolved remote; branch-config precedence over a coexisting `origin`; a stale branch-configured remote; the `.` sentinel; a detached `HEAD`; several remotes with no resolvable default; the remoteless case. **The new tests were checked against the pre-change helper to prove they are not vacuous.** Running the updated suite against the old script (same fixtures, unmodified) produces five discriminating failures: - `fresh bases on upstream/HEAD, not local HEAD` — the exact bug in the issue - `warning names the resolved remote's symref` - `warning offers set-head for that remote` - `branch-configured remote wins over origin` - `ambiguous remotes name the no-default-remote cause` The precedence test asserts against **three distinct commits** (origin tip, upstream tip, local `HEAD`) so it cannot pass whichever remote is picked. Guard-only assertions (detached `HEAD`, remoteless, stale/`.` fallthrough) pass under both scripts by design — they are regression guards, not discriminators. Other gates, all green: `shellcheck -x` clean on both scripts; `scripts/check-shell-portability.sh` introduces no new hits (the two reported are pre-existing lines, unchanged and only shifted); `scripts/check-skill-portability.sh` clean; `scripts/check-changelog-parity.sh --check` and `--check-bump origin/main` pass; `scripts/validate-plugins.sh` passes; `markdownlint-cli2` reports 0 errors. Independent fresh-context review ran with the implementation rationale withheld; every finding it raised was fixed before this PR. Two of its catches are worth naming: two new tests could have passed on a failing helper (an empty `$out` degrades the probed path to `/UNPUSHED.md`, which is absent) and now carry exit + positive-existence guards, and the new warning templating had no discriminating assertion at all — the case that covers it is the `upstream/HEAD not set` / `set-head upstream --auto` pair above. Its shellcheck pass also caught a defect this change introduced: the added `--help` text used backticks inside the unquoted `usage()` heredoc, which would have executed `git clone -o upstream` on every `--help`. Fixed; `--help` now renders the text literally. ## Related - Raised by a Codex review thread on #898 (the PR for #399, Phase A), which added this helper. - **Deliberately not addressed:** `parse_owner_repo`'s remote lookup in the same file still probes `origin` only, so in a `-o upstream` clone the owner/repo directory name degrades to the repo-directory name. It is outside this issue's acceptance criteria, and changing it would move existing worktree directory names — a separate decision from the base-ref fix. - **Pre-existing, not introduced here:** a *dangling* `<remote>/HEAD` symref (present, but its target ref absent — e.g. after an upstream default-branch rename) makes `symbolic-ref` succeed, so git receives a non-existent ref and the helper exits 4 with a message about the branch already existing. The old code had the identical hole. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

Summary
Phase A of #399: unify worktree creation behind one shared helper so
/worktree createno longer lands in Claude Code's in-repo.claude/worktrees/(the nested placement that triggers the confirmed CLAUDE.md/rules double-load bug, #400). This ships the helper + the skill routing; the nativeWorktreeCreatehook (Phase B) is intentionally not shipped here — it stays gated on two empirical upstream gates (see Phase B boundary below).Implements the operator ruling recorded on #398 (2026-07-21): central-external-root placement, with the root stored in a new
worktree_rootuserConfig directory key mirroring the existingbabysit_worktree_rootshape.What changed
plugins/source-control/scripts/worktree-create.sh— the single owner of worktree creation:<root>/<owner>-<repo>-<slug>(owner/repo parsed from the origin remote via a named function that handles GitHub, GitLab subgroups, and Azure DevOps_git/v3markers; falls back to the repo-dir name when there is no remote).--nameup front against theEnterWorktreeschema (≤64 chars, each/-segment[A-Za-z0-9._-]) and refuses invalid names (exit 2) rather than lettinggit worktree addfail opaquely. The branch is used verbatim; only the directory slug transforms (/→-).worktree.baseReffresh(default branch resolved symbolically viaorigin/HEAD, never a hardcodedorigin/main) orhead; warns on stderr when it must fall back to local HEAD.git worktree add, then reimplements Claude Code's.worktreeincludecopy — the intersection of.worktreeinclude-matched and gitignored files (that copy is bypassed when a worktree is created withgit worktree adddirectly). NUL-delimited enumeration; copy failures are reported and surfaced (exit 4), not silently counted.worktree_rootuserConfig directory key (plugin.json). Its description encodes refuse-with-guidance — when unset,/worktree createrefuses and points at setup; it does not silently fall back to the in-repo default./worktree createpatched (SKILL.md+context/create.md): run the helper, thenEnterWorktree(path:)into the created worktree. On any non-zero helper exit it STOPs and never falls back toEnterWorktree(name:). Documents the out-of-.claude/worktrees/approval prompt (not suppressible outsidebypassPermissions) and the declined-approval recovery.worktree-create.test.sh): path computation + slug, refuse-when-unconfigured (empty root and unexpanded${user_config.*}token), https/scp/Azure/GitLab URL parsing, base-reffresh/head(incl. uncached-origin/HEADfallback warning),.worktreeincludeintersection, trailing-valueless-flag guard, unsafe/over-long name refusal,--repo-diroutside a repo, trailing-slash root.Refuse-behavior evidence
With
worktree_rootunset (empty or the literal${user_config.worktree_root}token), the helper exits 3 and prints, to stderr:The skill treats exit 3 as a hard stop — it never falls through to the in-repo path.
Phase B boundary (not in this PR)
The native
WorktreeCreatehook routes the same helper, but only once both empirical gates pass:worktree_name; reality is reportedly.name): upstream anthropics/claude-code #77566..claude/worktrees/) prompts/blocks under normal permissions: upstream anthropics/claude-code #78212. Confirmed for the interactive path here (the entry prompts; acceptable because the user is present), but the autonomous path needs #78212 resolved before the hook can ship.Phase-B precondition surfaced in review: the helper's flag CLI is the stable seam, but the hook adapter will need its own validation of the stdin→flag mapping (base-ref derivation,
.namesanitization) before it can be trusted — the helper's guards cover the flags it receives, not the adapter that produces them.Verification
Local gates all green on the rebased branch: shellcheck,
worktree-create.test.sh(37 cases), portability-lint, changelog-parity (--check-bump), validate-plugins, markdownlint, skill-quality (PASS). Signed commits. An independent fresh-context review pass ran before this PR; its CRITICAL (arg-loop hang on a valueless trailing flag) and four IMPORTANT findings are all addressed in the second commit.Closes #399
Related
worktree_rootuserConfig key) is what this PR implements. feat(source-control): machine-agnostic worktree setup skill (detect -> decide -> install) #398 remains open (it owns detect→decide→install and the hook).🤖 Generated with Claude Code