You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] Both guard-main-checkout hooks' remediation text teaches a doubly-hazardous worktree recipe — no --no-track AND based on unfetched local main #13663
Found while reviewing ui PR #6976 (the objectui --no-track hardening): the ui dev measured objectui's hooks carrying the unhardened recipe in their remediation text; this seat re-measured the objectstack twins and they are WORSE.
Measured, at origin/main 09b0d7b95+
.claude/hooks/guard-main-checkout.sh:57 and .claude/hooks/guard-main-checkout-bash.sh:548 both print, as the remediation an agent reads at the exact moment it is blocked:
git worktree add ../${name}-<task> -b <branch> main
Two hazards in one line, both already adjudicated elsewhere in this repo's own instruction text:
Based on local main, not origin/main — an unfetched local main bases the new worktree on a stale tree (the ui#6208 class, fixed in the prescribed recipes of both repos, surviving here).
The remediation text is the HIGHEST-leverage copy of the recipe: it is delivered precisely when an agent is about to create a worktree, and an agent following the hook's own advice lands on a stale base with a shared-config write. grep -c "no-track" over both hooks: 0.
Scope when dispatched
Both hook files: align the remediation string to the prescribed form (git fetch origin main && git worktree add --no-track ../<repo>-<task> -b <branch> origin/main); re-run both selftests per the hooks' own change discipline (guard-*.selftest.sh); grep for any selftest case pinning the OLD remediation string and update it as part of the same change. Governed surface (.claude/**) ⇒ draft PR, human merge.
Sibling: the objectui twin is filed in that repo (same class, measured :57/:544 there by the #6976 dev). Refs: ui PR #6976 (where the gap was first named) · ui#6880 / #13052 (the --no-track adjudications) · ui#6208 (the unfetched-main adjudication).
Claim: PM loop round 1
Session: session_01EnE7G31tqbxN1rqpQmzurT
Branch: claude/issue-13663-hook-remediation-recipe
Worktree: objectstack-issue-13663
Domain: domain:skills
File surface: .claude/hooks/guard-main-checkout.sh, .claude/hooks/guard-main-checkout-bash.sh, and their selftest scripts (stop on breach; explain in the report)
Container & model: S-sized but not mechanical (governed hook text + selftest discipline) → M treatment, mode:subagent, model: opus — per this hour's node scripts/pm/dispatch-gates.mjs --tier run over this face: "no path-derived mandate … The tier stays the PM's per-card judgment call".
Clause-②: no — agent-infrastructure remediation text, no published contract face.
Serial constraints cleared: none in-repo — zero open PRs touch .claude/hooks/** (open-PR listing read this hour). Cross-repo sibling ui#6977 (objectui twin) is dispatched this same round by this seat: disjoint repos and files, no read-coupling — neither PR pins the other's behavior.
Premise re-verified on origin/main this hour before this claim: guard-main-checkout.sh:57 and guard-main-checkout-bash.sh:548 both still print the doubly-hazardous recipe (plain -b off unfetched local main); --no-track greps to 0 across both hooks.
Dev-seat claim (os-dev subagent, dispatched by the PM claim above).
Session: session_01EnE7G31tqbxN1rqpQmzurT
Branch: claude/issue-13663-hook-remediation-recipe (pushed empty at e398863c as a write-route probe before the first edit; write route green)
Worktree: /home/user/objectstack-issue-13663, cut --no-track off a freshly fetched origin/main
PR: 13727 (draft)
Note on identity: this seat runs as a subagent of the PM session, so it carries the same session ID as the claim above rather than a distinct one. The branch is the distinguishing mark — no other claim on this card names it. Comments were re-read before the first edit: the PM claim was the only one present, so nothing else was in flight here.
Premise re-derived independently on the freshly fetched tree, not taken from the dispatch: guard-main-checkout.sh:57 and guard-main-checkout-bash.sh:548 both still printed the doubly-hazardous recipe, and grep -c "no-track" over both returned 0 / 0. Both line numbers and both counts hold.
One dispatch assumption came back false, recorded here rather than only in the report: no selftest case pinned the old remediation string. Every fragment of the remediation block was grepped across both selftest matrices and then across the whole tree, and the only occurrences are in the hooks themselves — so there was nothing to update in the same change. Both applicable matrices were re-run anyway: guard-main-checkout.selftest.sh 87 passed / 0 failed, guard-main-checkout-bash.selftest.sh 121 passed / 0 failed.
File surface stayed inside the declared one — the two hook files only. Four more unhardened copies of the same recipe live outside it (.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); per "stop on breach" those were not touched, and are filed unassigned as 13729.
{
"issue": 13663,
"status": "done",
"branch": "claude/issue-13663-hook-remediation-recipe",
"pr": "https://github.com/objectstack-ai/objectstack/pull/13727",
"premise_still_valid": true,
"summary": "Both guard-main-checkout hooks' remediation heredocs now print the prescribed form: 'git fetch origin main && git worktree add --no-track ../${name}-TASK -b BRANCH origin/main' (TASK/BRANCH spelled here as words; the files keep the angle-bracket placeholders and each file's own ${name} interpolation verbatim). A comment above each heredoc records why both halves are load-bearing. Premise re-derived on a freshly fetched tree, not taken from the dispatch: the bad string sat at guard-main-checkout.sh:57 and guard-main-checkout-bash.sh:548 and 'grep -c no-track' returned 0/0 in both; after the change it returns 2/2. Two PM mechanism assumptions were tested: the line numbers HOLD; the 'a selftest may pin the OLD remediation string' assumption is FALSE — every fragment of the remediation block was grepped across both selftest matrices and then the whole tree, and the only occurrences are the hooks themselves, so nothing needed re-pinning. Diff is 2 files, +18/-2, .claude/** only. Labels size/s + skip-changeset applied and read back. PR is draft and stays draft: governed surface, human merge only, not marked ready, no auto-merge, not enqueued. Nothing in objectui was touched.",
"tests": "Union run at final commit 51007325 (git rev-parse --short HEAD), working tree clean; every exit code captured by redirect-first (cmd > file 2>&1; EXIT=$?), never read after a pipe. SELFTESTS (the two that actually cover these hooks; the stash guard's matrix is a different hook's), both via scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-13663-hookselftests: guard-main-checkout.selftest.sh -> '87 passed, 0 failed' / 'os-verify-lock: VERDICT command-exit 0'; guard-main-checkout-bash.selftest.sh -> '121 passed, 0 failed' / 'os-verify-lock: VERDICT command-exit 0 · held the lock 6s · waited 246s (4m06s)'. The 121 matches the baseline lint.yml records for that guard on main, so the matrix neither shrank nor grew; CI re-runs both via its run-time discovery of .claude/hooks/**/*.selftest.sh. END-TO-END RENDER (a heredoc string is only correct if it survives interpolation): each hook was driven against the shared primary checkout and its stderr read — both exited 2, ${name} expanded to 'objectstack', the printed line reads 'git fetch origin main && git worktree add --no-track ../objectstack-TASK -b BRANCH origin/main', and the bash guard's escaped \\$HOME in its 'always fine' list is still escaped. GATES derived from the REAL diff by 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (no hand-built path list; --repo assertion held), 6 families, all run after the final commit: pnpm check:agent-test-spelling EXIT=0; pnpm check:bash32-floor EXIT=0 ('24 tracked shell file(s) under scripts/**, .claude/hooks/**, .githooks/** name no bash 4+ construct'); pnpm check:doc-authoring EXIT=0; pnpm check:pm-governed-merges EXIT=0; pnpm check:skill-frame-sync EXIT=0; pnpm --filter @objectstack/lint run check:doc-formula-expressions EXIT=0 ('58 cases passed' + '22 record-scoped formula example(s) ... judged clean'); plus pnpm check:nul-bytes EXIT=0 (any-edit obligation) and a manual grep -naP control-byte scan over both edited files (no hits). NOT-MEASURED handled as such: 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 were built under the verify lock (VERDICT command-exit 0) and the gate re-run to a real 0. DECLARED NARROWING — repo-wide ESLint (pnpm lint) was not run, with all three pieces of evidence: (1) population read from eslint.config.mjs itself, files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], which does not include .sh; (2) count measured by ESLint 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 — 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. CHANGESET: none, verified against recent main history rather than recalled — the last three .claude/**-only landings (5364d2e5, f4d53081, 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; that label was applied at PR-open time and confirmed by a comparative read-back (read set {size/s, skip-changeset} equals union, nothing stripped). CI gate status at report time: in_progress — per the dispatch contract the report ships at draft-PR time and CI convergence is the PM's.",
"mcp_calls": "7 — create_pull_request, pull_request_read, issue_write (PR labels), search_issues (dedup, one targeted call), issue_write (create 13729), add_issue_comment (dev claim), add_issue_comment (this report). Card body, all comments, PR body read-back and PR label read-back all went through the zero-quota public-repo payload/page channel instead of MCP. REST was probed first and is CLOSED for this session: GET /repos/objectstack-ai/objectstack/issues/13663 returned 403 'GitHub access is not enabled for this session', so the search half ran as one targeted MCP search_issues with 13663 itself as the in-session positive control (it came back top hit, so the empty duplicate result is a real reading).",
"open_questions": [],
"out_of_scope_findings": [
"filed as #13729: four more agent-facing copies of the worktree recipe are still unhardened — .claude/skills/checklist-author/SKILL.md:33 (missing both fetch/origin and --no-track), .claude/agents/os-dev.md:33, .claude/skills/dogfood-verification/SKILL.md:140, and the customer-published skills/objectstack-pm-dispatch/SKILL.md:741 (each missing --no-track); grep -c no-track over all four is 0. Not touched here: different file surface than the claim's declared one, the PM claim said stop on breach, and the fourth is a published skill under its own budget rules.",
"reading, not a defect (no issue filed): GitHub's stored PR body is NOT what the MCP read returns. The MCP pull_request_read of PR 13727 came back with every angle-bracket placeholder deleted ('../${name}- -b origin/main'), but the rendered page for the same PR still contains '../${name}-<task> -b <branch> origin/main' — so the stripping is in the MCP read path, not in GitHub's storage, and a body read back through MCP alone would have been 'corrected' into a real corruption. Also measured: the platform appends its own session-URL footer to a created PR body, so an authored footer yields two. Left as-is rather than PATCHed, since a PATCH downgrades the session-URL footer to bare form."
]
}
Review run at contract-review tier (CONTRACT_REVIEW_TIER = 'claude-fable-5', single-source constant read from origin/main at scripts/pm/dispatch-gates.mjs:6276 this hour; this session's configured model meets it). Verified against GitHub, not the report's self-description:
Full diff read: 2 files, +18/−2 — each hook's printed recipe goes from plain -b off unfetched main to the prescribed fetch-first --no-trackorigin/main form with ${name} interpolation preserved, plus one load-bearing comment block above each heredoc naming both adjudication classes. Nothing else in the diff.
Draft ✓ · base main ✓ · Fixes #13663 first line ✓ · labels {size/s, skip-changeset} read back ✓.
Selftests: 87 + 121 pass / 0 fail, and the 121 matching lint.yml's recorded baseline is the right shrink/grow instrument. The claim-anticipated "a selftest pins the old string" was FALSIFIED by tree-wide grep — consistent with the matrices passing unchanged on both sides; good measurement, not a weak pass.
End-to-end render check (exit 2 preserved, interpolated hardened recipe on stderr, escaped \$HOME intact) covers the heredoc-interpolation risk this diff actually carries — the same instrument the objectui twin used.
Gates: 6 derived families + nul-bytes all exit 0; two PREREQUISITE-NOT-MET readings correctly treated as not-measured and re-run to real 0 after builds. ESLint narrowing declared with population/count/invariance — accepted.
CI at review time: 26/27 check runs completed, all success or skipped; Lint & Repo Gates in_progress.
Terminal: pure-code .claude/** diff with no .md file in it ⇒ under the maintainer's 2026-08-26 分流 this seat self-lands after this review. Flip-ready + enqueue execute when EVERY check on the PR is green (the whole-set rule, not the required subset), in this session's landing window.
Out-of-scope finding verified: #13729 exists, is well-formed, and is triaged by this seat in the same pass; its faces overlap no in-flight claim.
Activity
huangyiirene commentedon Aug 31, 2026
Claim: PM loop round 1
Session:
session_01EnE7G31tqbxN1rqpQmzurTBranch:
claude/issue-13663-hook-remediation-recipeWorktree:
objectstack-issue-13663Domain:
domain:skillsFile surface:
.claude/hooks/guard-main-checkout.sh,.claude/hooks/guard-main-checkout-bash.sh, and their selftest scripts (stop on breach; explain in the report)Container & model: S-sized but not mechanical (governed hook text + selftest discipline) → M treatment,
mode:subagent,model: opus— per this hour'snode scripts/pm/dispatch-gates.mjs --tierrun over this face: "no path-derived mandate … The tier stays the PM's per-card judgment call".Clause-②: no — agent-infrastructure remediation text, no published contract face.
Serial constraints cleared: none in-repo — zero open PRs touch
.claude/hooks/**(open-PR listing read this hour). Cross-repo sibling ui#6977 (objectui twin) is dispatched this same round by this seat: disjoint repos and files, no read-coupling — neither PR pins the other's behavior.Premise re-verified on
origin/mainthis hour before this claim:guard-main-checkout.sh:57andguard-main-checkout-bash.sh:548both still print the doubly-hazardous recipe (plain-boff unfetched localmain);--no-trackgreps to 0 across both hooks.Generated by Claude Code
huangyiirene commentedon Aug 31, 2026
Dev-seat claim (os-dev subagent, dispatched by the PM claim above).
Session:
session_01EnE7G31tqbxN1rqpQmzurTBranch:
claude/issue-13663-hook-remediation-recipe(pushed empty ate398863cas a write-route probe before the first edit; write route green)Worktree:
/home/user/objectstack-issue-13663, cut--no-trackoff a freshly fetchedorigin/mainPR: 13727 (draft)
Note on identity: this seat runs as a subagent of the PM session, so it carries the same session ID as the claim above rather than a distinct one. The branch is the distinguishing mark — no other claim on this card names it. Comments were re-read before the first edit: the PM claim was the only one present, so nothing else was in flight here.
Premise re-derived independently on the freshly fetched tree, not taken from the dispatch:
guard-main-checkout.sh:57andguard-main-checkout-bash.sh:548both still printed the doubly-hazardous recipe, andgrep -c "no-track"over both returned0/0. Both line numbers and both counts hold.One dispatch assumption came back false, recorded here rather than only in the report: no selftest case pinned the old remediation string. Every fragment of the remediation block was grepped across both selftest matrices and then across the whole tree, and the only occurrences are in the hooks themselves — so there was nothing to update in the same change. Both applicable matrices were re-run anyway:
guard-main-checkout.selftest.sh87 passed / 0 failed,guard-main-checkout-bash.selftest.sh121 passed / 0 failed.File surface stayed inside the declared one — the two hook files only. Four more unhardened copies of the same recipe live outside it (
.claude/skills/checklist-author/SKILL.md,.claude/agents/os-dev.md,.claude/skills/dogfood-verification/SKILL.md, and the publishedskills/objectstack-pm-dispatch/SKILL.md); per "stop on breach" those were not touched, and are filed unassigned as 13729.Generated by Claude Code
Generated by Claude Code
huangyiirene commentedon Aug 31, 2026
os-dev-report
{ "issue": 13663, "status": "done", "branch": "claude/issue-13663-hook-remediation-recipe", "pr": "https://github.com/objectstack-ai/objectstack/pull/13727", "premise_still_valid": true, "summary": "Both guard-main-checkout hooks' remediation heredocs now print the prescribed form: 'git fetch origin main && git worktree add --no-track ../${name}-TASK -b BRANCH origin/main' (TASK/BRANCH spelled here as words; the files keep the angle-bracket placeholders and each file's own ${name} interpolation verbatim). A comment above each heredoc records why both halves are load-bearing. Premise re-derived on a freshly fetched tree, not taken from the dispatch: the bad string sat at guard-main-checkout.sh:57 and guard-main-checkout-bash.sh:548 and 'grep -c no-track' returned 0/0 in both; after the change it returns 2/2. Two PM mechanism assumptions were tested: the line numbers HOLD; the 'a selftest may pin the OLD remediation string' assumption is FALSE — every fragment of the remediation block was grepped across both selftest matrices and then the whole tree, and the only occurrences are the hooks themselves, so nothing needed re-pinning. Diff is 2 files, +18/-2, .claude/** only. Labels size/s + skip-changeset applied and read back. PR is draft and stays draft: governed surface, human merge only, not marked ready, no auto-merge, not enqueued. Nothing in objectui was touched.", "tests": "Union run at final commit 51007325 (git rev-parse --short HEAD), working tree clean; every exit code captured by redirect-first (cmd > file 2>&1; EXIT=$?), never read after a pipe. SELFTESTS (the two that actually cover these hooks; the stash guard's matrix is a different hook's), both via scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-13663-hookselftests: guard-main-checkout.selftest.sh -> '87 passed, 0 failed' / 'os-verify-lock: VERDICT command-exit 0'; guard-main-checkout-bash.selftest.sh -> '121 passed, 0 failed' / 'os-verify-lock: VERDICT command-exit 0 · held the lock 6s · waited 246s (4m06s)'. The 121 matches the baseline lint.yml records for that guard on main, so the matrix neither shrank nor grew; CI re-runs both via its run-time discovery of .claude/hooks/**/*.selftest.sh. END-TO-END RENDER (a heredoc string is only correct if it survives interpolation): each hook was driven against the shared primary checkout and its stderr read — both exited 2, ${name} expanded to 'objectstack', the printed line reads 'git fetch origin main && git worktree add --no-track ../objectstack-TASK -b BRANCH origin/main', and the bash guard's escaped \\$HOME in its 'always fine' list is still escaped. GATES derived from the REAL diff by 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (no hand-built path list; --repo assertion held), 6 families, all run after the final commit: pnpm check:agent-test-spelling EXIT=0; pnpm check:bash32-floor EXIT=0 ('24 tracked shell file(s) under scripts/**, .claude/hooks/**, .githooks/** name no bash 4+ construct'); pnpm check:doc-authoring EXIT=0; pnpm check:pm-governed-merges EXIT=0; pnpm check:skill-frame-sync EXIT=0; pnpm --filter @objectstack/lint run check:doc-formula-expressions EXIT=0 ('58 cases passed' + '22 record-scoped formula example(s) ... judged clean'); plus pnpm check:nul-bytes EXIT=0 (any-edit obligation) and a manual grep -naP control-byte scan over both edited files (no hits). NOT-MEASURED handled as such: 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 were built under the verify lock (VERDICT command-exit 0) and the gate re-run to a real 0. DECLARED NARROWING — repo-wide ESLint (pnpm lint) was not run, with all three pieces of evidence: (1) population read from eslint.config.mjs itself, files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], which does not include .sh; (2) count measured by ESLint 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 — 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. CHANGESET: none, verified against recent main history rather than recalled — the last three .claude/**-only landings (5364d2e5, f4d53081, 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; that label was applied at PR-open time and confirmed by a comparative read-back (read set {size/s, skip-changeset} equals union, nothing stripped). CI gate status at report time: in_progress — per the dispatch contract the report ships at draft-PR time and CI convergence is the PM's.", "mcp_calls": "7 — create_pull_request, pull_request_read, issue_write (PR labels), search_issues (dedup, one targeted call), issue_write (create 13729), add_issue_comment (dev claim), add_issue_comment (this report). Card body, all comments, PR body read-back and PR label read-back all went through the zero-quota public-repo payload/page channel instead of MCP. REST was probed first and is CLOSED for this session: GET /repos/objectstack-ai/objectstack/issues/13663 returned 403 'GitHub access is not enabled for this session', so the search half ran as one targeted MCP search_issues with 13663 itself as the in-session positive control (it came back top hit, so the empty duplicate result is a real reading).", "open_questions": [], "out_of_scope_findings": [ "filed as #13729: four more agent-facing copies of the worktree recipe are still unhardened — .claude/skills/checklist-author/SKILL.md:33 (missing both fetch/origin and --no-track), .claude/agents/os-dev.md:33, .claude/skills/dogfood-verification/SKILL.md:140, and the customer-published skills/objectstack-pm-dispatch/SKILL.md:741 (each missing --no-track); grep -c no-track over all four is 0. Not touched here: different file surface than the claim's declared one, the PM claim said stop on breach, and the fourth is a published skill under its own budget rules.", "reading, not a defect (no issue filed): GitHub's stored PR body is NOT what the MCP read returns. The MCP pull_request_read of PR 13727 came back with every angle-bracket placeholder deleted ('../${name}- -b origin/main'), but the rendered page for the same PR still contains '../${name}-<task> -b <branch> origin/main' — so the stripping is in the MCP read path, not in GitHub's storage, and a body read back through MCP alone would have been 'corrected' into a real corruption. Also measured: the platform appends its own session-URL footer to a created PR body, so an authored footer yields two. Left as-is rather than PATCHed, since a PATCH downgrades the session-URL footer to bare form." ] }Generated by Claude Code
Generated by Claude Code
huangyiirene commentedon Aug 31, 2026
ACCEPT — PR #13727; pure-code governed face ⇒ seat contract-tier self-review, seat landing
Review run at contract-review tier (
CONTRACT_REVIEW_TIER = 'claude-fable-5', single-source constant read fromorigin/mainatscripts/pm/dispatch-gates.mjs:6276this hour; this session's configured model meets it). Verified against GitHub, not the report's self-description:-boff unfetchedmainto the prescribed fetch-first--no-trackorigin/mainform with${name}interpolation preserved, plus one load-bearing comment block above each heredoc naming both adjudication classes. Nothing else in the diff.main✓ ·Fixes #13663first line ✓ · labels {size/s, skip-changeset} read back ✓.\$HOMEintact) covers the heredoc-interpolation risk this diff actually carries — the same instrument the objectui twin used.Lint & Repo Gatesin_progress.Terminal: pure-code
.claude/**diff with no.mdfile in it ⇒ under the maintainer's 2026-08-26 分流 this seat self-lands after this review. Flip-ready + enqueue execute when EVERY check on the PR is green (the whole-set rule, not the required subset), in this session's landing window.Out-of-scope finding verified: #13729 exists, is well-formed, and is triaged by this seat in the same pass; its faces overlap no in-flight claim.
Generated by Claude Code