Skip to content

fix(guard): cumulative-cwd cd-target resolution for both guard hooks - #287

Merged
Data-Wise merged 8 commits into
devfrom
feature/guard-cd-resolution
Jul 15, 2026
Merged

fix(guard): cumulative-cwd cd-target resolution for both guard hooks#287
Data-Wise merged 8 commits into
devfrom
feature/guard-cd-resolution

Conversation

@Data-Wise

Copy link
Copy Markdown
Owner

Summary

PR A of SPEC-guard-hardening-adversarial-review-2026-07-15.md. Adversarially re-verified (not assumed) the two items deferred after #284:

  • The originally-suspected "no-switch-guard.sh has the same compound-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.
  • The real, confirmed bug: no-switch-guard.sh's git_dir resolution only honored -C <path>, never a leading cd <path> && — so is_dirty() checked the session's own repo, not the actual switch target.
  • Separately confirmed: 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 leading cd/-C — a multi-hop chain (cd a && cd b && ...) resolves to the wrong repo.

Changes

  • Cumulative-cwd cd-target resolution added to both scripts/no-switch-guard.sh (new) and scripts/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).
  • Multi-hop + cross-context test coverage added to both tests/test_branch_guard.sh and tests/test_no_switch_guard.sh.
  • docs/reference/REFCARD-BRANCH-GUARD.md — new "Cross-Context Target Resolution" section (no pre-existing section to extend).
  • CHANGELOG (CHANGELOG.md + docs/CHANGELOG.md) [Unreleased] entries.
  • Follow-up hardening commit: awk char-class split for cross-awk safety.

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)
  • Differential test confirmed meaningful: multi-hop cases FAIL against base dev (picks the first cd, wrong target), PASS against this branch
  • Fail-open verified: empty stdin and a quoted-separator command both exit 0, no crash
  • Independently re-verified twice outside the implementing agent's own report — once after an ambient ~/.claude/guards.json mute (unrelated to this branch) cleared, once via HOOK_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

Data-Wise and others added 8 commits July 15, 2026 11:10
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>
@Data-Wise
Data-Wise merged commit ebd27fe into dev Jul 15, 2026
6 checks passed
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 15, 2026
ORCHESTRATE-*.md files belong on feature branches during development,
not on dev after merge (missed during the #287/#288 squash-merge cleanup).

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
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>
@Data-Wise
Data-Wise deleted the feature/guard-cd-resolution branch July 24, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant