fix(guard): cumulative-cwd cd-target resolution for both guard hooks - #287
Merged
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the single-hop `-C`-or-leading-`cd` git_dir extraction with a clause-walking resolver: each bare `cd <path>` retargets every subsequent clause (so `cd a && cd b && git switch x` resolves to b, not a), and a `git -C <path>` sets that invocation's target against the effective cwd. Last retarget wins. Phase 1.1 judgment call (per ORCHESTRATE): parallel-inline logic in each hook, NOT a shared sourced file. Two reasons: (1) the hooks derive their base cwd differently — no-switch from process $PWD, branch-guard from the JSON `.cwd` — a shared fn would be parameterized on that anyway; (2) a shared file would force both install scripts to deploy + source it, adding source-path-resolution fragility. Inline avoids both. Split uses `awk gsub` (real newline on BSD & GNU; BSD sed's `\n` does not). Paths containing `$`/backtick/quote are skipped (shell expansions / quoted mid-command `;cd` in commit messages we can't statically resolve). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 2.2/2.3: is_dirty() already checks `git -C "$git_dir" status` against the resolver's target (verified). Add a [target repo: <name>] note to the dirty-tree confirm and clean-switch announce so a cross-context switch is unambiguous about which repo it acts on — mirrors branch-guard.sh's "name the resolved repo/branch explicitly" checklist item from #284. Note is empty when no cd/-C retarget occurred (same-repo switches unchanged). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 3: retrofit #284's single-hop §8d0 resolver to cumulative clause tracking. The old block resolved only ONE leading `cd`/`-C` (regex anchored `^cd`, structurally unable to match a second cd). Replace with the same left-to-right clause walk used in no-switch-guard.sh: each bare `cd <path>` retargets every subsequent clause (cd a && cd b && git push → b), a `git -C <path>` sets that invocation's target, last retarget wins. Downstream re-derivation (IS_CROSS_REPO_TARGET, BRANCH, PROJECT_ROOT, INTEGRATION_BRANCH, PROTECTION) fires against the LAST resolved target, guarded by the existing -d/git-repo/branch checks — a spurious target from a quoted mid-command separator that isn't a real git repo on a different branch is simply ignored. Scope comment updated (v1→v2); quote-bearing paths now also skipped. All #284 single-hop cross-context tests stay green (121/121, Phase 3.3 regression check). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 4. branch-guard: cd a && cd b && git push (last-wins → b), mixed cd + -C (b wins), and inverse cd feature && cd main (last-wins gates the protected target). no-switch: BRAINSTORM Context Scan scenarios — dirty session + clean worktree target (NOT blocked), clean session + dirty worktree target (blocked), and pure multi-hop cd dirty && cd clean (last-wins → NOT blocked). Verified the multi-hop cases FAIL against the base dev scripts (which pick the first cd) and PASS against the cumulative resolver — meaningful, failable coverage. Suites: branch-guard 124/124, no-switch 39/39. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 5. REFCARD-BRANCH-GUARD.md: new "Cross-Context Target Resolution" section (resolution table + cumulative note + documented quote-parsing limitation). CHANGELOG.md + docs/CHANGELOG.md: mirrored [Unreleased] Fixed entry. markdownlint clean; changelogs verified identical. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace gsub(/&&|;|\|/) with gsub(/&&|[;|]/) in both hooks' cumulative resolvers. `\|`-as-literal-pipe is well-defined on BSD awk (dev box) but POSIX-underspecified for other awks; a character class is unambiguous across BSD/gawk/mawk and costs nothing — matters for the macOS + Ubuntu-CI portability constraint. Behavior identical; suites stay 124/124 + 39/39. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2 tasks
Data-Wise
added a commit
that referenced
this pull request
Jul 15, 2026
All three guard-hardening PRs merged to dev with green CI: #287 (cd-target resolution), #288 (guards.json lock, no CHANGELOG conflict with #287 despite both touching it), #289 (orchestrate-dispatch hardening). Worktrees removed, local branches deleted via safe `-d` (squash-merge content recognized without needing force). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Data-Wise
added a commit
that referenced
this pull request
Jul 16, 2026
Three claims in GRILL-branch-guard-target-resolution-2026-07-14.md are now stale given this session's guard-hardening work: the disproven no-switch-guard "compound-string-match/worktree-cleanup" framing, the concurrency-safety item marked deferred (now fixed in #288, broader than originally scoped), and the cd/-C cumulative-tracking question marked unresolved (now locked and shipped in #287). Added forward pointers to the actual resolutions rather than leaving a future session to re-derive them from a stale doc. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Data-Wise
added a commit
that referenced
this pull request
Jul 16, 2026
…k-decision test A/B timing against branch-guard.sh at 3 historical commits (pre-guard-rewrites, pre-#284, pre-#287) all showed 207-360ms on this machine — the block-decision path was never reliably under 200ms, independent of today's cumulative-cwd work. Loosened the budget to 350ms with the evidence recorded inline; left the allow-decision budget at 200ms since it passes reliably.
Data-Wise
added a commit
that referenced
this pull request
Jul 16, 2026
Data-Wise
added a commit
that referenced
this pull request
Jul 16, 2026
Phase 3.6 is closed and committed; the branch is deliberately parked, not abandoned. Records why it is not PR-ready so the next session does not re-derive the diagnosis. Scope reality: this branch is the whole v4 train (36 commits, Phases 3+3.5+3.6), 41 behind dev off a 2026-07-09 merge-base — not a small Workstream B+C PR. Three CI blockers with three unrelated causes: 1. test_branch_guard.sh — stale branch, NOT a regression. A characterization test encoding a known limitation that guard-hardening PRs #287-289 closed on dev; dev already renamed it `..._now_caught` expecting 2. The suite runs the INSTALLED hook at $HOME/.claude/hooks/, so its result is machine-global state, not branch state — which is why it passes on dev and fails here with identical code. Fixed free by a dev merge. 2. ci.yml:92 command floor 60 vs actual 46 — the open T3.4, stale a second time (targeted 86->60, then Phase 3.5 cut to 46). CI hard-fails. The new number is a decision (mass-deletion guardrail), not a mechanical edit. 3. test_git_shim_correctness.sh — the only true branch debt. T3.5.2 deleted commands/git/ but left the suite asserting those 7 shims exist. A dev merge does NOT fix it. Needs the suite updated/removed + its ci.yml:180 line. Root blind spot: these are bash suites (ci.yml:177-180); pytest does not run them. T3.5.2 cited "full pytest 2575 passed" as evidence and shipped this. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Data-Wise
added a commit
that referenced
this pull request
Jul 16, 2026
Brings 41 commits of dev (guard-hardening PRs #287-289, the cwd resolver, and /craft:git:issue-check from PR #283) onto the v4 folio-split branch. 34 conflicts resolved. Surface: 47 commands / 40 skills / 2 agents. Conflict resolution: - ~30 count-cascade conflicts (dev 95/45/8 vs branch 46/40/2) -> took the branch's post-split surface, then recomputed every tracked count mechanically via bump-version.sh --counts-only (14 files) rather than trusting hand-resolution. validate-counts.sh exits 0 at 47/40/2. - commands/docs/quickstart.md (modify/delete) -> kept deleted; folio owns it (verified present at folio/commands/docs/quickstart.md). dev's only change was a count bump inside an example template. - commands/git/issue-check.md -> KEPT AS A LIVE COMMAND, not folded into skills/dev/git/. Git's directory-rename detection had auto-relocated it to skills/dev/git/references/ (our side renamed commands/git/ -> skills/dev/git/references/ in T3.5.2), which would have broken tests/test_issue_check_unit.py: that test hardcodes commands/git/issue-check.md and exec()s the classify_issue() block out of it, so the command file is the single source of truth for the classifier. Same structural constraint recorded for commands/ci/triage.md in GRILL D13. Restored to commands/git/; its 12 unit tests pass. - skills/dev/git/SKILL.md -> kept the branch's reference-doc rows, dropped dev's stale commands/git/docs/refcard.md row. Operation 13 (Issue Premise Check) merged in cleanly from dev. - mkdocs.yml nav -> dropped dev's 7 deleted git command entries, kept the issue-check entry (blind --ours would have dropped it from the site). Also corrects two now-false claims in skills/dev/git/SKILL.md ("commands/git/ is now empty", "there is no explicit /craft:git:* invocation path anymore") — issue-check is a live command and the skill now says so, with the reason. Long-tail doc counts (46 -> 47) updated in 6 live user-facing files. Historical changelog entries, illustrative sample outputs, and spec/brainstorm records deliberately left at their original numbers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR A of
SPEC-guard-hardening-adversarial-review-2026-07-15.md. Adversarially re-verified (not assumed) the two items deferred after #284:main-substring / worktree-cleanup bug as pre-fix(guard): resolve Bash cross-context targets in branch-guard.sh #284 branch-guard.sh" does not reproduce — disproven by direct code read, dropped from scope.no-switch-guard.sh'sgit_dirresolution only honored-C <path>, never a leadingcd <path> &&— sois_dirty()checked the session's own repo, not the actual switch target.branch-guard.sh's own fix(guard): resolve Bash cross-context targets in branch-guard.sh #284-era §8d0 resolver (line 579, anchored^cd) only resolves a single leadingcd/-C— a multi-hop chain (cd a && cd b && ...) resolves to the wrong repo.Changes
cd-target resolution added to bothscripts/no-switch-guard.sh(new) andscripts/branch-guard.sh(retrofit of already-merged fix(guard): resolve Bash cross-context targets in branch-guard.sh #284 code) — parallel-inline logic in each (judgment call: the hooks derive base cwd differently, a shared sourced file would force both install scripts to deploy+source it).tests/test_branch_guard.shandtests/test_no_switch_guard.sh.docs/reference/REFCARD-BRANCH-GUARD.md— new "Cross-Context Target Resolution" section (no pre-existing section to extend).CHANGELOG.md+docs/CHANGELOG.md)[Unreleased]entries.Test plan
bash tests/test_branch_guard.sh— 124/124 (was 121; +3 multi-hop)bash tests/test_no_switch_guard.sh— 39/39 (was 36; +3 cross-context)dev(picks the firstcd, wrong target), PASS against this branch~/.claude/guards.jsonmute (unrelated to this branch) cleared, once viaHOOK_SCRIPT=<absolute-path>against the repo copy directly (the classifier correctly blocked reinstalling unmerged code to the live hook for test convenience)🤖 Generated with Claude Code