Repository navigation
source-control: worktree create aborts on zsh after creating the worktree, and fresh branches track origin/main #5983
Description
Severity: high (auditor-suggested, uncalibrated)
Component: source-control:worktree, create action: context/create.md, scripts/worktree-create.sh, and its consumer skills/pull-request/scripts/push-branch.sh (source at worktree HEAD 0c5fd28; plugin version 0.73.0)
Readiness: mixed: agent-ready E1, E2, E3, I2; needs-decision E4, E6, Q1; S1 is an adjacent repository-config note
Duplicates: none found; Related: #904 (closed): the fresh-base remote resolution whose start point E1 tracks; #965 (closed): worktree_root shell safety, which introduced the temp-file render E3, E6 and Q1 concern; #2209 (closed): the worktree-create gate's exit taxonomy.
Summary
A session ran /source-control:worktree create on Claude Code 2.1.288 (WSL2, zsh). The helper
created the worktree, but the new feature branch was set to track origin/main, so a bare
git push fails, git status reports the branch as ahead of main for its whole life, and the
plugin's own push-branch.sh keeps that upstream after the first push. The temp directory the
create render needs was left behind because the cleanup rm was denied, and the mandatory
worktree.baseRef read was refused by this repository's Read deny with no fallback in the skill.
A reproduction outside the session also shows that the prescribed helper block aborts on any zsh
host right after it creates and locks the worktree, because it assigns to zsh's read-only status.
Fixing these makes create leave a correctly tracked branch, finish cleanly on bash and zsh, and
stop leaving files for the operator to remove.
Findings
E1. Fresh-base worktrees leave the feature branch tracking origin/
- Severity: medium Status: recommendation (tier-0, primary https://git-scm.com/docs/git-config)
- What happened: the helper printed
branch 'feat/5778-mod-conventions' set up to track 'origin/main'.(raw/parent-transcript-create.md L165);@{u}isorigin/mainandbranch.<name>.mergeisrefs/heads/main(raw/git-tracking-state.txt). Reproduced on a scratch repo with a local bare remote: a baregit pushexits 128 under the defaultpush.default=simple, andgit status -sbshows...origin/main [ahead 1](raw/tracking-repro-output.txt; evidence-auditor-repro.md v2; evidence-seam-probes.md). The--base-ref headarm sets no upstream. - Why:
scripts/worktree-create.sh:975sets the start point torefs/remotes/<remote>/<default>and:999runsgit worktree add -b "$name" "$worktree_path" "$base_commit"without--no-track, so git's defaultbranch.autoSetupMerge=truerecords the remote default branch as upstream. The helper also serveshooks/worktree-create-gate.sh, so harness-driven worktrees get the same tracking (read from code, not observed). - Options, cheapest first: 1. add
--no-trackto thegit worktree add -bcall at :999 (reproduced: the base still equalsorigin/main, no upstream is set, andpush-branch.shthen setsorigin/<branch>; evidence-auditor-repro.md v3). 2.git -c branch.autoSetupMerge=false worktree add -b ...(reproduced, v2 last arm). 3. With either, add a test inscripts/worktree-create.test.shassertingbranch.<name>.mergeis unset after a fresh create. Suggested: 1 plus 3. - Scope check: needs to exist (root cause of E2, reproduced three times); option 1 is a one-token change. No consumer in this plugin relies on
@{u}being the default branch.
E2. push-branch.sh keeps the helper-written upstream as if the user chose it
- Severity: medium Status: recommendation (tier-0, primary https://git-scm.com/docs/git-config)
- What happened: after
push-branch.sh,@{u}staysorigin/mainandgit status -sbstill shows...origin/main [ahead 1](raw/tracking-repro-output.txt; evidence-auditor-repro.md v2).push.autoSetupRemote=trueon this machine does not help, because git applies it only when no upstream exists. - Why:
skills/pull-request/scripts/push-branch.sh:22-33and:69pass-uonly whenbranch.<name>.remoteand.mergeare both unset, on the premise that any existing upstream is one the user chose. For a helper-created branch the helper set it (E1). - Options, cheapest first: 1. fix E1; with
--no-trackthe same script setsorigin/<branch>(v3). 2. Document incontext/create.mdthat worktrees created before the fix can be repaired withgit branch --unset-upstreambefore the first push. 3. Teach push-branch.sh to retarget an upstream equal to the remote default branch on a differently named branch, printing one line. Suggested: 1 plus 2. - Scope check: option 3 adds a heuristic to a script whose premise is otherwise sound; 1 plus 2 covers new and existing branches.
E3. The prescribed create block aborts on zsh after the worktree is created (reproduction, not a session failure)
- Severity: high Status: recommendation (tier-1, primary https://zsh.sourceforge.io/Doc/Release/Parameters.html)
- What happened: the session did not hit this; it had already split the block into separate calls after an unrelated denial (raw/parent-transcript-create.md L135-171). In the audit, a Bash-tool probe on this host (zsh 5.9) of
true; status=$?; echo after; exit 0returned exit 1 and(eval):1: read-only variable: status(evidence-auditor-probes.md). The seam reviewer then ran the whole block with the real helper in one zsh invocation on a scratch repo: the helper created and locked the worktree and printed its path, the script aborted atstatus=$?with exit 1, and the temp directory remained. Create.md step 2 says to stop on a non-zero exit, so the session would not enter the worktree; a same-name retry exits 4 with "target path already exists" (evidence-seam-probes.md). - Why:
context/create.md:104-111prescribes one block (helper call,status=$?,rm -rf "<root_dir>",exit "$status"), and:117explainsstatus=$?as the exit-propagation device.statusis a read-only special parameter in zsh, and the Bash tool runs zsh when that is the user's shell, as on this host. - Options, cheapest first: 1. rename the variable (
rc=$?...exit "$rc"), which works in bash and zsh. 2. Move the temp-file cleanup into the helper (E6 option 1) so the block is the bare helper call and its exit status is the helper's own. 3. Add a lint or test that rejects assignments to zsh read-only names (status,pipestatus) in skill and context bodies. Suggested: 1 now; 2 if E6 option 1 is adopted. - Scope check: needs to exist on every zsh host that runs the block as written; option 1 is a two-line edit, and option 3 is optional once the variable is gone.
E4. The mandatory worktree.baseRef read has no path when the read is refused
- Severity: medium Status: needs-decision
- What happened: the session's
grep -c '"baseRef"' <repo>/.claude/settings.local.jsonwas denied (raw/parent-transcript-create.md L144-145; an earlier combined call was denied at L135-136), and the session proceeded without--base-ref, which defaults to fresh. This repository's.claude/settings.json:9deniesRead(./.claude/settings.local.json)(raw/permission-rules.txt). Whether the denial came from that rule or from the auto-mode classifier cannot be told from the message. - Why:
context/create.md:38requires reading the effectiveworktree.baseRef, local settings first, and warns that skipping the read "silently forcesfresh", but says nothing for a read that is refused. - Options, cheapest first: 1. Add the refusal path to create.md: name the layer that could not be read, say the base defaults to fresh, and ask (interactive) or state the assumption in the result (unattended). 2. Resolve the setting inside the helper (
--base-ref auto, reading the settings files with jq and printing only the resolved word). Caveat: a helper subprocess is outside Read deny rules, so this routes around a deny the repository set on purpose (see S1). - Scope check: matters only where a repository denies the read and a user sets
headin local settings; option 1 is one sentence.
E6. Temp-dir cleanup was denied and the directory leaked
- Severity: low Status: needs-decision
- What happened: the session ran
rm -rf /tmp/tmp.uikV6LUdK2unquoted as its own call and the user deny ruleBash(rm -rf /*)refused it (raw/parent-transcript-create.md L169-171); the directory, holding a 28-byte unexpanded${user_config.worktree_root}token and a 63-byte data path, is still on disk (raw/permission-rules.txt) and was carried as a cleanup item for the user for the rest of the session. Note: the quoted spelling create.md:109 prescribes passed the same rule in an auditor probe (evidence-auditor-probes.md), and on a zsh host the prescribed block never reachesrm(E3), so both paths leave the directory. - Why: the render (
context/create.md:89-111) leaves cleanup to a model-writtenrm -rfof an absolute path, which a user's rules may match depending on spelling, and nothing else removes the directory when that step fails or never runs. - Options, cheapest first: 1. Have the helper delete the two files it was handed and then
rmdirtheir parent, behind a flag such as--consume-input-files; the skill block then contains normand no exit-status variable (also closes E3 and Q1). A seam probe on 2.1.288 showed the same unquotedrm -rf <abs>denied when written as the command and run without denial from inside a launched script (evidence-seam-probes.md). 2. Skill-only: replacerm -rf "<root_dir>"withrm -f "<root_dir>/worktree-root" "<root_dir>/data-root"andrmdir "<root_dir>", as separate calls. Auditor's suggested default: 1. - Scope check: needed for a leak-free create on any rule set; option 2 is smaller but is still a model-written
rm.
I2. worktree-create.sh sources two libraries without a failure guard
- Severity: low Status: recommendation (tier-1, primary https://www.gnu.org/software/bash/manual/bash.html)
- What happened: in an auditor reproduction that copied
scripts/withouthooks/, the run continued past the failedsourceto an empty worktree path, git printedBUG: builtin/worktree.c:464: How come '' becomes empty after sanitization?, and the helper reported "branch ... may already exist", which was not the cause (evidence-auditor-repro.md v2, patched arm). - Why:
scripts/worktree-create.sh:65and:70callsourcewith no|| exit, unlike:60, which exits 4 whenlib/worktree-facts.shfails to load. The helper runsset -uo pipefailwithout-e(:57), and bash'ssourceof a missing file returns non-zero and the script continues (evidence-seam-probes.md, bash 5.3.9). - Options, cheapest first: 1. add
|| { echo "error: cannot load <lib>" >&2; exit 4; }to both lines, matching :60. - Scope check: tiny and consistent with :60; it matters only for an incomplete install.
Q1. The create render costs four or more tool calls, a temp dir, and a cleanup the operator may end up owning
- Severity: quality of life Status: needs-decision
- What happened:
context/create.md:89-111is four steps (mktemp block, twoWritecalls, helper block) plus the baseRef read. The session made seven calls with two denials up to the helper run and eight with three denials through the cleanup, and then carried the leftover directory as a user action item (raw/parent-transcript-create.md L135-186). - Why: the render design in create.md:89-111.
- Options, cheapest first: 1. state in create.md that each block is its own Bash call. 2. With E6 option 1, the render becomes mktemp, two Writes and one helper call with no cleanup step.
- Scope check: no separate change if E6 option 1 lands.
S1. Adjacent note (repository config, not the plugin): this repo denies Read on .claude/settings.local.json
- Severity: standards (adjacent) Status: needs-decision (repository settings owner)
- What happened: this repository's
.claude/settings.json:9carriesRead(./.claude/settings.local.json); that rule is what refused the skill's baseRef read in this session (raw/permission-rules.txt; raw/parent-transcript-create.md L144-145). - Why it is raised: the standards repository keeps this deny off its own permission floor and explains why: the file holds personal overrides, not credentials, and a Read deny also blocks Edit and Write on it (
melodic-software/standardscomponents/claude-permissions/README.md:39; its testclaude-permissions.test.sh:95-107checks the floor config, not this repository). - Options: 1. drop the deny from this repository's settings. 2. keep it, and rely on E4 option 1 for the refusal path. No plugin change follows from this note.
- Scope check: one line in repository settings, outside the plugin.
Open questions for maintainers
- E4, refused baseRef read. Decision: what create does when a settings layer cannot be read. Options: (a) document the refusal path in create.md (name the layer, default to fresh, ask interactively or state the assumption unattended); (b) resolve
worktree.baseRefinside the helper, which reads files outside Read deny rules and so routes around a deny the repository set on purpose. Auditor's suggested default: (a). Unblocks: the E4 fix and an honest result line for unattended runs. - E6 and Q1, who owns the temp files. Decision: whether the helper deletes the input files it is handed. Options: (a) helper-side deletion behind a flag, which also removes the block's exit-status variable (E3) and the cleanup call (Q1); (b) skill-only
rm -fplusrmdiras separate calls. Auditor's suggested default: (a). Research note: a tier-0 probe on 2.1.288 supports (a), but the claim that permission rules do not see the commands a launched script runs failed publish-grade verification (the vendor docs are undated and the only current corroborators are probes on one host), so a probe on a second host (macOS) would settle it. Unblocks: E6, Q1, and the simpler form of the E3 fix. - S1, this repository's Read deny. Decision for the repository settings owner: keep or drop
Read(./.claude/settings.local.json). Options: (a) drop it, matching the standards floor; (b) keep it and rely on the E4 refusal path. Auditor's suggested default: none; the auditor routes it to the settings owner, since it is a permissions choice outside the plugin. Unblocks: the baseRef read in this repository regardless of how E4 is decided.
Research
Fetched 2026-10-02 unless noted; saved under the packet's sources/.
- E1: tier 1, https://git-scm.com/docs/git-config, supports ("automatic setup is done when the starting point is a remote-tracking branch"); tier 1, https://git-scm.com/docs/git-worktree, supports (the
-bstart point marked as upstream); tier 1, https://git-scm.com/docs/git-branch, supports the fix (--no-track: "Do not set up "upstream" configuration"). Tier 0: transcript L165, raw/tracking-repro-output.txt, evidence-auditor-repro.md v2 and v3, evidence-seam-probes.md. Dissent: none found. - E2: tier 1, https://git-scm.com/docs/git-config, supports (
push.autoSetupRemoteapplies only when no upstream exists); tier 1, https://git-scm.com/docs/git-push, supports (argument-less push uses the upstream); tier 1, https://git-scm.com/docs/git-branch, supports the fix. Tier 0: raw/tracking-repro-output.txt, evidence-auditor-repro.md v2 and v3. Dissent: none found. - E3: tier 1, https://zsh.sourceforge.io/Doc/Release/Parameters.html, supports (
statuslisted among parameters that "are read-only"); tier 1, https://man.archlinux.org/man/zshparam.1.en, supports (same manual text, so one source pool); tier 1, https://code.claude.com/docs/en/tools-reference.md, supports in part (Claude Code sources the user's shell startup file "depending on your shell"; it does not by itself say which shell executes). Tier 0: evidence-auditor-probes.md and evidence-seam-probes.md (zsh=5.9 bash=none, the abort, end to end). Supporting at MEDIUM for the claim that zsh runs Bash tool commands wherever it is the login shell, including the macOS default: https://code.claude.com/docs/en/env-vars.md (CLAUDE_CODE_SHELL entry: auto-detection uses$SHELLwhen it points to bash or zsh; page undated) and [BUG]Bashtool executes the user's login shell (e.g. /bin/zsh) and should thus be called "shell" not "bash"; LLM trips over bash-only idioms anthropics/claude-code#91498 (macOS, Claude Code 2.1.258, open). Dissent: none found. - I2: tier 1, https://www.gnu.org/software/bash/manual/bash.html (Edition 5.3, 2025-05-18), supports (
./source: if the file "is not found, or cannot be read, the return status is non-zero"; the exit is a POSIX-mode rule); tier 1, https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html (POSIX 2024), supports the POSIX-mode half ("a non-interactive shell shall abort"); tier 2, https://linuxize.com/post/bash-source-command/ (updated 2026-09-05; read through a scraper after HTTP 403), supports ("source returns exit code 1 and prints an error"); tier 0, evidence-seam-probes.md (bash 5.3.9: default mode continues with rc=1,--posixexits). Independent verifier: pass. - Not tiered: E6's and E4's harness claims (Bash rules match only the text Claude writes; Read deny covers recognized Bash readers that name the file) are stated in https://code.claude.com/docs/en/permissions.md and the changelog, but the verifier failed them for publish use; details in the packet's
research/.
Review seams
- skill-quality:check: run by the audit session; PASS, 0 errors, 3 warnings (raw/skill-quality-check.txt).
- Research: used;
/discovery:researchran inline (the invoking context was a subagent); coverage and source-applicability gates exit 0; verifier: fail rows 4,7,12 on the harness claims, pass on the bash claim, so only I2 moved to tier-1. - Breadth review: fell back;
review:fanoutandreview:quality-gatereview diffs, not a findings ledger; structured self-review per finding instead. - Adversarial: fell back, no dedicated skill; findings re-derived from artifacts and source; E3 reproduced end to end.
- Upstream conformance: fell back;
grep -Fre-check of the auditor's own quoted spans (14 of 14 found), not an independent conformance skill. - Adaptation chapter: Opus 5.5 chapter read; no conflict with the suggested instruction text.
- Scope challenge: fell back; one line per suggested change (see each finding).
Adversarial disagreements (severities above stand as uncalibrated):
- E2 is the downstream effect of E1, not an independent defect; consider folding it into E1.
- E4's medium looks too high: it needs a repository Read deny plus a
headvalue set only in local settings, and the packet shows no such value. Suggest low. - E6 reads as if the prescribed command was denied; the session's denial hit its own unquoted spelling, while the prescribed quoted form passed the same rule. The leak stands on both paths.
- E1: the risk that an agent runs git's suggested
git push origin HEAD:mainis an inference, not an observation; git offers it alongsidegit push origin HEAD. - E3: a retry exits 4 with "target path already exists", not "branch may already exist" as the audit first said; and E3 was not hit in the session.
- I2 triggers only on an incomplete install.
- S1: the standards repository keeps the deny off its own floor; it does not check this repository's settings, so "forbids" overstated it.
- E3's research record: one corroborator is the same zsh manual on a mirror and the other measures startup-file sourcing; the executing-shell half rests on host probes.
Evidence
- Packet:
/home/kyle/worktrees/melodic-software-claude-code-plugins-feat-5778-mod-conventions/.work/plugin-quality/evidence/f5c77f0d-9bcd-492e-a773-1fa651e926a0/source-control-worktree/20261002T233255Z/ - Key files:
raw/parent-transcript-create.md,raw/git-tracking-state.txt,raw/tracking-repro-output.txt,raw/permission-rules.txt,evidence-auditor-repro.md,evidence-auditor-probes.md,evidence-seam-probes.md,audit-notes-2.md(ledger, revision 2),seam-review.md,research/. - Transcript:
/home/kyle/.claude/projects/-home-kyle-worktrees-melodic-software-claude-code-plugins-feat-5778-mod-conventions/f5c77f0d-9bcd-492e-a773-1fa651e926a0.jsonl(create at 2026-10-02T21:56Z). - Environment: Claude Code 2.1.288, Linux WSL2, Bash tool shell zsh 5.9 (bash 5.3.9 for scripts).
- Candidates without a session artifact are kept in the packet's
contract.mdand are not part of this item. - This item was produced by an unattended audit; nothing in it was confirmed by a human.
Activity
kyle-sexton commentedon Oct 4, 2026
This was generated by AI during triage.
Triage
Outcome: human-gated, needs-decision. Priority medium (auditor severity: high, uncalibrated). Work class: scoped (C3, bug-fix).
Verified on current main (unchanged since the audit):
git worktree add -b <name> <path> <base_commit>with no--no-track, where the base is the remote default branch, so git recordsorigin/<default>as the new branch's upstream.status=$?(a read-only parameter in zsh) and then runsrm -rfon the temp directory.worktree-root-resolve.shandworktree-path-lib.shwithout a failure guard, unlike theworktree-facts.shline before them, which exits 4..claude/settings.local.json, the file the create spoke requires reading forworktree.baseRef.Basis: verified,
git show origin/main:plugins/source-control/scripts/worktree-create.shlines 60, 65, 70, 999;plugins/source-control/skills/worktree/context/create.mdlines 104-111;.claude/settings.jsonline 9.Why a human: three open decisions, one of which is a permission rule in this repository's settings. If the maintainer approves the recommended answers below, the plugin work becomes scoped work an agent can pick up (flip to
agent-ready+work-class: scoped+status: ready, dropneeds-humanandstatus: needs-decision). The settings decision (3) is the maintainer's own edit either way.Decisions for the maintainer
worktree.baseRefread is refused. Options: (a) document the refusal path: name the settings layer that could not be read, say the base defaults to fresh, ask in an interactive session or state the assumption in the result when unattended; (b) resolve the setting inside the helper, which reads files outside Read deny rules and so routes around a deny the repository set on purpose. Recommended: (a). Unblocks: an honest result line for unattended runs.rmand no exit-status variable (this also removes the zshstatusproblem and two tool calls); (b) skill-only: rename the variable torcand replacerm -rfwithrm -fof the two files plusrmdir, as separate calls. Recommended: (a). Unblocks: a leak-free create on any permission rule set, and the simpler zsh fix..claude/settings.local.json(repository settings, not the plugin). Options: (a) drop it, matching the standards repository's permission floor, which leaves it out because the file holds personal overrides, not credentials; (b) keep it and rely on decision 1's refusal path. Recommended: none from triage; this is a permissions choice for the settings owner. Unblocks: the baseRef read in this repository.Agent Brief
Type: Bug
Summary: Make
/source-control:worktree createleave a correctly tracked branch, finish cleanly on bash and zsh, and leave no temp files behind.Current behavior:
origin/<default>, so a baregit pushfails underpush.default=simple,git statusreports the branch ahead of the default branch for its whole life, andpush-branch.shkeeps that upstream because it only sets-uwhen no upstream exists.status=$?assignment after the worktree is already created and locked; the skill then stops, and a same-name retry fails with "target path already exists".rmis denied or never reached.Desired behavior:
git worktree add -bcall passes--no-track, so no upstream is set and the firstpush-branch.shrun setsorigin/<branch>. The--base-ref headpath is unchanged.sourcelines exit 4 with a "cannot load " message, matching the existing guarded one.git branch --unset-upstreamas the repair for branches created before the fix, and the refused-read path per decision 1.Key interfaces:
worktree-create.shexit codes: unchanged (4 stays the environment-error code).push-branch.sh: unchanged; its "set-uonly when no upstream exists" rule stays.Acceptance criteria:
branch.<name>.mergeis unset after a fresh-base create and still passes for theheadbase.Out of scope:
push-branch.shto retarget an existing upstream.kyle-sexton commentedon Oct 13, 2026
Decision recorded. The owner approved applying independently verified recommendations (high-risk set, reviewed) on 2026-10-13.
Basis: plugins/source-control/skills/worktree/context/create.md:38
Basis: plugins/source-control/skills/worktree/context/create.md:110-111,119; source-control: worktree create aborts on zsh after creating the worktree, and fresh branches track origin/main #5983 E6
Basis: melodic-software/standards components/claude-permissions/README.md:39-42; .claude/settings.json:26
Fresh-base worktrees track origin/, the create block assigns zsh's read-only status, temp files leak, and two source lines lack a guard. All verified unchanged on main (worktree add at line 1023 has no --no-track; create.md:110 status=$?).
Verified by an independent fresh-context check (confirmed, risk high): gh issue view 5983 (OPEN, triage comment); origin/main plugins/source-control/scripts/worktree-create.sh:66-72 (unguarded source), :1023 (worktree add -b without --no-track); origin/main plugins/source-control/skills/worktree/context/create.md:38,110-111,119; origin/main .claude/settings.json:26 Read(./.claude/settings.local.json) deny; gh api melodic-software/standards components/claude-permissions/README.md:39-42; https://code.claude.com/docs/en/permissions.md line 353 (Read deny also blocks Edit and Write)
Admission to an agent lane stays with the work-class rules and owner ratification.