Skip to content

fix(hooks): align both guard-main-checkout remediation recipes to the prescribed worktree form - #13727

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-13663-hook-remediation-recipe
Aug 31, 2026
Merged

fix(hooks): align both guard-main-checkout remediation recipes to the prescribed worktree form#13727
os-zhuang merged 1 commit into
mainfrom
claude/issue-13663-hook-remediation-recipe

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #13663

Both guard-main-checkout hooks printed the unhardened worktree recipe as their remediation text — the copy an agent reads at the exact moment it is blocked, and therefore the highest-leverage copy of the recipe in the repo. It carried two hazards, both already adjudicated and both already hardened in AGENTS.md Prime Directive 11 and in CLAUDE.md; only these two copies were left behind.

  • No --no-track — plain -b writes branch.NAME.remote / branch.NAME.merge into the .git/config that every worktree of the repo SHARES (the 13052 / ui 6880 adjudications).
  • Based on local main, with no fetch — an unfetched local main bases the new tree on a stale commit (the ui 6208 adjudication).

What changed

Two files, one printed line each, plus a comment above each heredoc recording why both halves are load-bearing so the next reader does not "simplify" them back out.

  • .claude/hooks/guard-main-checkout.sh (the Edit/Write/NotebookEdit guard)
  • .claude/hooks/guard-main-checkout-bash.sh (the same write arriving through Bash)

Each file's own ${name} interpolation is preserved verbatim — the printed recipe is now, in both:

git fetch origin main && git worktree add --no-track ../${name}-<task> -b <branch> origin/main
cd ../${name}-<task> && pnpm install    # then re-run your edits there

What was measured, not assumed

Premise re-derived on a freshly fetched tree (not taken from the card): the bad string sits at guard-main-checkout.sh:57 and guard-main-checkout-bash.sh:548, and grep -c "no-track" over both files returns 0 / 0. Both hold.

No selftest case pinned the old string. The card anticipated one might. Grepping both selftest matrices — and then the whole tree — for every fragment of the remediation block (-b <branch>, re-run your edits there, re-run the command there, dedicated worktree:) puts the only two occurrences in the hooks themselves. Nothing else asserts on this text, so nothing else needed updating. The two selftests that do cover these hooks are guard-main-checkout.selftest.sh and guard-main-checkout-bash.selftest.sh (the stash guard's matrix is a different hook's); both were re-run:

guard-main-checkout.selftest.sh        87 passed, 0 failed
guard-main-checkout-bash.selftest.sh  121 passed, 0 failed

The bash guard's 121 matches the baseline lint.yml records for it on main, so the matrix neither shrank nor grew. CI runs both anyway — the Claude hook guard self-tests step discovers .claude/hooks/**/*.selftest.sh at run time.

The remediation actually renders. A string inside a heredoc is only correct if it survives interpolation, so each hook was driven end to end against the shared primary checkout and its stderr read:

⛔ Blocked: editing on the shared PRIMARY checkout, not a worktree.
   repo: /home/user/objectstack  (branch: claude/pm-dispatch-skills-9th9j4)
...
  git fetch origin main && git worktree add --no-track ../objectstack-<task> -b <branch> origin/main
  cd ../objectstack-<task> && pnpm install    # then re-run your edits there

Exit code 2 preserved in both, ${name} expands to the repo basename, and the bash guard's escaped \$HOME in the "always fine" list is still escaped.

Gates

Derived from the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (which reads its own change set from the merge base — no hand-built path list), then run at the final commit 51007325. Exit codes captured with a redirect before the read, never through a pipe:

gate exit
pnpm check:agent-test-spelling 0
pnpm check:bash32-floor 0
pnpm check:doc-authoring 0
pnpm check:pm-governed-merges 0
pnpm check:skill-frame-sync 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0
pnpm check:nul-bytes (any edit) 0

check:bash32-floor is the one that reads these files as shell: "24 tracked shell file(s) under scripts/**, .claude/hooks/**, .githooks/** name no bash 4+ construct…". check:doc-formula-expressions first answered PREREQUISITE NOT MET — @objectstack/formula is not built, then the same for @objectstack/lint; that is a not-measured reading, not a red gate, so both packages were built and the gate re-run to a real 0.

ESLint (pnpm lint, which CI runs unconditionally) was not run repo-wide, and this is a declared narrowing with all three pieces of evidence:

  1. Population, read from eslint.config.mjs rather than guessed: files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']. .sh is not in it.
  2. Count, measured by ESLint itself via --format json over exactly the two changed paths: 2 results, 0 errors, 2 warnings, both "File ignored because no matching configuration was supplied." — i.e. this diff contributes zero files to ESLint's population.
  3. Invariance for untouched files: the diff changes no JS/TS and does not touch eslint.config.mjs, and a shell file cannot enter a type-aware program, so every untouched file's verdict is identical to main's.

No changeset

This diff is .claude/** only and publishes nothing from any package, so it takes the skip-changeset route. Verified against recent main history rather than recalled — the last three .claude/**-only landings (5364d2e5, f4d53081, and the .claude/** half of 3d089745) carry no .changeset/*.md, and pr-automation.yml's Check Changeset job re-reads labels live and exempts on an exact-match skip-changeset. The label is applied to this PR.

Governed surface

.claude/** is governed surface: this PR stays draft and is for human merge only. Not marked ready, no auto-merge, not enqueued.

Out of scope, filed separately

The same unhardened recipe survives in four more agent-facing copies (.claude/skills/checklist-author/SKILL.md, .claude/agents/os-dev.md, .claude/skills/dogfood-verification/SKILL.md, and the published skills/objectstack-pm-dispatch/SKILL.md). Those are a different file surface, one of them is a published skill with its own budget rules, and this card's claim declared the two hook files — so they are filed rather than ridden along here. The objectui twin (ui 6977) is being handled in that repo by another dev; nothing here touches objectui.

Refs: 13663 · ui 6976 · 13052 / ui 6880 · ui 6208.

Generated by Claude Code


Generated by Claude Code

… prescribed worktree form

The remediation text is the highest-leverage copy of the worktree recipe: it
reaches an agent at the exact moment it is about to create a worktree. Both
hooks printed the unhardened form -- plain `-b` (which writes
branch.NAME.remote/merge into the .git/config every worktree of the repo
SHARES) off an unfetched local `main` (which bases the new tree on a stale
commit). Both halves were already adjudicated and already hardened in
AGENTS.md PD#11 and CLAUDE.md; only these two copies were left behind.

Printed recipe is now, in both hooks, with each file's own `${name}`
interpolation preserved verbatim:

  git fetch origin main && git worktree add --no-track ../${name}-<task> -b <branch> origin/main

A comment above each heredoc records why both halves are load-bearing, so the
next reader does not "simplify" them back out.

Verified by rendering each hook end to end against the shared primary checkout
(exit 2, `${name}` expands, `\$HOME` still escaped) and by both hermetic
selftest matrices: guard-main-checkout 87 passed / 0 failed,
guard-main-checkout-bash 121 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@huangyiirene huangyiirene added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026 — with Claude
@huangyiirene
huangyiirene marked this pull request as ready for review August 31, 2026 08:53
@huangyiirene
huangyiirene enabled auto-merge August 31, 2026 08:53
@huangyiirene
huangyiirene added this pull request to the merge queue Aug 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@huangyiirene
huangyiirene added this pull request to the merge queue Aug 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 31, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 3d6e74b Aug 31, 2026
29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-13663-hook-remediation-recipe branch August 31, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants