fix(hooks): print the --no-track worktree recipe in both guards' remediation text - #7000

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track
Aug 31, 2026
Merged

fix(hooks): print the --no-track worktree recipe in both guards' remediation text#7000
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6977

What changed

Both worktree guards print a remediation recipe when they block — the copy an agent reads
at the exact moment it is about to act, and therefore the copy most likely to be run
verbatim. Both printed the recipe with a plain -b. PR #6976 hardened the prescribed form
in CLAUDE.md and AGENTS.md with --no-track; the hooks' copy was left behind.

One word added per file, net ±0 lines in both:

  • .claude/hooks/guard-main-checkout.sh:57
  • .claude/hooks/guard-main-checkout-bash.sh:544

Both now emit exactly the prescribed form (verbatim from a live invocation of each hook,
with $name interpolated):

 git fetch origin main && git worktree add --no-track ../objectui-TASK -b BRANCH origin/main
cd ../objectui-TASK && pnpm install # then re-run your edits there

Why

The hazard is the one PR #6976 established: plain -b writes the new branch's upstream
keys (branch.NAME.remote, branch.NAME.merge) into the one .git/config that every
linked worktree of a repo shares. That write can fail after the branch is created,
leaving a branch with no worktree — a half state the error text does not name. Read as
"the worktree exists", the agent starts editing the shared primary checkout, which is the
one thing these two guards exist to prevent.

Scope — additive, not a recipe rewrite

The premise was re-verified on origin/main before the first edit and it is narrower than
the sibling card's. This repo's hooks already emitted the fetch-hardened base
(git fetch origin main && … origin/main, landed for ui#6208), so the missing flag was the
only surviving divergence from the prescribed form. Measured on the freshly fetched tree:
grep -rc "no-track" .claude/hooks/ returned 0 across all seven files there (four guards,
three self-test matrices) before this change, and 1 in each of the two edited guards after.

Not in this PR

Each of these was examined and deliberately left alone:

  • .changeset/6208-worktree-recipe-fetch-base.md quotes the pre-ui#6208 recipe as the
    record of a past change. Editing a historical changeset rewrites that record —
    adjudicated in PR docs: harden the worktree recipe with --no-track; correct the stale no-mechanical-backstop claim #6976, and restated on the card. check-changeset-overwrite.mjs
    confirms it mechanically: 0 modified, 0 deleted.
  • The two -cmp comparison-tree lines in CLAUDE.md / AGENTS.md create no branch and
    take an explicit ref from the caller, so there are no upstream keys to write and the flag
    does not apply. This is the same reading ui#6208 recorded for them.
  • .claude/hooks/guard-tree-enum.selftest.sh:99 is a command payload fed to a different
    guard, asserting that ordinary commands pass through it. It is test input, not text any
    agent is told to run, and adding the flag there would change no coverage.

Verification

All at efbfb15, the head of this branch. Nine checks, joined with && so one verdict
covers all of them — exit 0.

Hook self-tests — the only gate that targets .claude/hooks/**
(.github/workflows/hook-selftests.yml), all three matrices it runs:

self-testresult
guard-main-checkout-bash.selftest.sh121 passed, 0 failed
guard-shared-stash.selftest.sh41 passed, 0 failed
guard-tree-enum.selftest.sh36 passed, 0 failed

Identical to the pre-edit baseline, and that is the expected direction rather than a weak
result: no self-test case pins the remediation string, so none needed updating in this
change. Measured, not assumed — a grep -rn for four distinctive fragments of the two block
messages (fetch origin main, dedicated worktree, Blocked: editing on the shared,
WRITES into the shared) across .claude/hooks/*.selftest.sh and scripts/ returns no
match in any self-test.

A second measurement, reported because its absence is the answer rather than an assumption:
this repo ships noguard-main-checkout.selftest.sh. .claude/hooks/ holds four guards
and three matrices; guard-main-checkout.sh is the one guard without one — so for that hook
the direct invocation below is the whole of its coverage.

Behavioural check — both hooks invoked directly with a payload targeting the shared
primary checkout. Each exits 2 (blocked, as designed) and prints the hardened recipe shown
above. The recipe it now prints was then fed back through all three of this repo's Bash
guards (guard-main-checkout-bash, guard-shared-stash, guard-tree-enum): all three exit
0, so the text is runnable and not self-blocked.

Repo gates that reach this diffcheck-control-bytes (5809 tracked text files
scanned), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite,
check-shell-escape-residue, check-governed-queue-guard --self-test (132 cases). All ✅.
bash -n clean on both edited files.

Measurement — nothing lints shell files' contents in this repo. Verified on this tree
rather than taken from the workflow header that claims it: eslint.config.js scopes every
block to **/*.{ts,tsx}, and there is no shellcheck step anywhere (the three matches in
the repo are two prose mentions and one # shellcheck source= directive inside
e2e/live/ci/start-backend.sh). check-shell-escape-residue reads fenced blocks in
AGENTS.md, CLAUDE.md, skills/ and content/docs/ — it does not reach .claude/hooks/.
So the self-test matrices plus the direct invocation above are the whole of the coverage
available for this diff, by measurement.

Changeset

check-changeset-presence reports no changeset is owed here (3 files changed, 0 of them
published source of a released package). One is included anyway, with empty frontmatter,
because that is this repo's verified convention for a diff of this shape — the same form as
.changeset/6880-worktree-recipe-no-track.md (PR #6976), 6208-worktree-recipe-fetch-base.md
and 6089-hook-selftests-step-names-drop-counts.md. The empty-frontmatter exemption is a
first-class pass in scripts/check-changeset-presence.mjs.

No skip-changeset label is applied: in this repo that label is a phantom.
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins that nothing under .github/ or
scripts/ may mention it, so applying it would exempt nothing.

Governed surface — draft, human merge

.claude/** is governed. check-governed-queue-guard --test on this diff returns
⛔ GOVERNED — 2 of 3 path(s), and one governed path governs the whole pull request. This PR
stays a draft: not flipped ready, not enqueued, no auto-merge armed. The merge is the
maintainer's, and a human merge is the review record.

Refs: PR #6976 · ui#6880 · ui#6208 · objectstack#13052. The objectstack twin
(objectstack#13663) covers that repo's hooks and is not addressed here.

Generated by Claude Code


Generated by Claude Code

…diation text
`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh`
each end their block message with the worktree recipe to run instead, and both created
the branch with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and
`AGENTS.md`; the hooks' copy was left behind — and it is the higher-leverage copy,
delivered at the exact moment an agent is about to act, so it is the one most likely to
be run verbatim.
The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
worktree of a repo shares. That write can fail after the branch is created, leaving a
branch with no worktree — a half state the error text does not name. Read as "the
worktree exists", the agent starts editing the shared primary checkout, which is the one
thing these two guards exist to prevent.
Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
(`git fetch origin main && … origin/main`, landed for ui#6208), so the missing flag was
the only surviving divergence from the prescribed form. One word added per file, net ±0
lines in both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 12:36
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit e55fdaeAug 31, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6977-hook-no-track branch August 31, 2026 13:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@huangyiirene@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(hooks): print the --no-track worktree recipe in both guards' remediation text - #7000

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track
Aug 31, 2026
Merged

fix(hooks): print the --no-track worktree recipe in both guards' remediation text#7000
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6977

What changed

Both worktree guards print a remediation recipe when they block — the copy an agent reads
at the exact moment it is about to act, and therefore the copy most likely to be run
verbatim. Both printed the recipe with a plain -b. PR #6976 hardened the prescribed form
in CLAUDE.md and AGENTS.md with --no-track; the hooks' copy was left behind.

One word added per file, net ±0 lines in both:

  • .claude/hooks/guard-main-checkout.sh:57
  • .claude/hooks/guard-main-checkout-bash.sh:544

Both now emit exactly the prescribed form (verbatim from a live invocation of each hook,
with $name interpolated):

 git fetch origin main && git worktree add --no-track ../objectui-TASK -b BRANCH origin/main
cd ../objectui-TASK && pnpm install # then re-run your edits there

Why

The hazard is the one PR #6976 established: plain -b writes the new branch's upstream
keys (branch.NAME.remote, branch.NAME.merge) into the one .git/config that every
linked worktree of a repo shares. That write can fail after the branch is created,
leaving a branch with no worktree — a half state the error text does not name. Read as
"the worktree exists", the agent starts editing the shared primary checkout, which is the
one thing these two guards exist to prevent.

Scope — additive, not a recipe rewrite

The premise was re-verified on origin/main before the first edit and it is narrower than
the sibling card's. This repo's hooks already emitted the fetch-hardened base
(git fetch origin main && … origin/main, landed for ui#6208), so the missing flag was the
only surviving divergence from the prescribed form. Measured on the freshly fetched tree:
grep -rc "no-track" .claude/hooks/ returned 0 across all seven files there (four guards,
three self-test matrices) before this change, and 1 in each of the two edited guards after.

Not in this PR

Each of these was examined and deliberately left alone:

  • .changeset/6208-worktree-recipe-fetch-base.md quotes the pre-ui#6208 recipe as the
    record of a past change. Editing a historical changeset rewrites that record —
    adjudicated in PR docs: harden the worktree recipe with --no-track; correct the stale no-mechanical-backstop claim #6976, and restated on the card. check-changeset-overwrite.mjs
    confirms it mechanically: 0 modified, 0 deleted.
  • The two -cmp comparison-tree lines in CLAUDE.md / AGENTS.md create no branch and
    take an explicit ref from the caller, so there are no upstream keys to write and the flag
    does not apply. This is the same reading ui#6208 recorded for them.
  • .claude/hooks/guard-tree-enum.selftest.sh:99 is a command payload fed to a different
    guard, asserting that ordinary commands pass through it. It is test input, not text any
    agent is told to run, and adding the flag there would change no coverage.

Verification

All at efbfb15, the head of this branch. Nine checks, joined with && so one verdict
covers all of them — exit 0.

Hook self-tests — the only gate that targets .claude/hooks/**
(.github/workflows/hook-selftests.yml), all three matrices it runs:

self-testresult
guard-main-checkout-bash.selftest.sh121 passed, 0 failed
guard-shared-stash.selftest.sh41 passed, 0 failed
guard-tree-enum.selftest.sh36 passed, 0 failed

Identical to the pre-edit baseline, and that is the expected direction rather than a weak
result: no self-test case pins the remediation string, so none needed updating in this
change. Measured, not assumed — a grep -rn for four distinctive fragments of the two block
messages (fetch origin main, dedicated worktree, Blocked: editing on the shared,
WRITES into the shared) across .claude/hooks/*.selftest.sh and scripts/ returns no
match in any self-test.

A second measurement, reported because its absence is the answer rather than an assumption:
this repo ships noguard-main-checkout.selftest.sh. .claude/hooks/ holds four guards
and three matrices; guard-main-checkout.sh is the one guard without one — so for that hook
the direct invocation below is the whole of its coverage.

Behavioural check — both hooks invoked directly with a payload targeting the shared
primary checkout. Each exits 2 (blocked, as designed) and prints the hardened recipe shown
above. The recipe it now prints was then fed back through all three of this repo's Bash
guards (guard-main-checkout-bash, guard-shared-stash, guard-tree-enum): all three exit
0, so the text is runnable and not self-blocked.

Repo gates that reach this diffcheck-control-bytes (5809 tracked text files
scanned), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite,
check-shell-escape-residue, check-governed-queue-guard --self-test (132 cases). All ✅.
bash -n clean on both edited files.

Measurement — nothing lints shell files' contents in this repo. Verified on this tree
rather than taken from the workflow header that claims it: eslint.config.js scopes every
block to **/*.{ts,tsx}, and there is no shellcheck step anywhere (the three matches in
the repo are two prose mentions and one # shellcheck source= directive inside
e2e/live/ci/start-backend.sh). check-shell-escape-residue reads fenced blocks in
AGENTS.md, CLAUDE.md, skills/ and content/docs/ — it does not reach .claude/hooks/.
So the self-test matrices plus the direct invocation above are the whole of the coverage
available for this diff, by measurement.

Changeset

check-changeset-presence reports no changeset is owed here (3 files changed, 0 of them
published source of a released package). One is included anyway, with empty frontmatter,
because that is this repo's verified convention for a diff of this shape — the same form as
.changeset/6880-worktree-recipe-no-track.md (PR #6976), 6208-worktree-recipe-fetch-base.md
and 6089-hook-selftests-step-names-drop-counts.md. The empty-frontmatter exemption is a
first-class pass in scripts/check-changeset-presence.mjs.

No skip-changeset label is applied: in this repo that label is a phantom.
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins that nothing under .github/ or
scripts/ may mention it, so applying it would exempt nothing.

Governed surface — draft, human merge

.claude/** is governed. check-governed-queue-guard --test on this diff returns
⛔ GOVERNED — 2 of 3 path(s), and one governed path governs the whole pull request. This PR
stays a draft: not flipped ready, not enqueued, no auto-merge armed. The merge is the
maintainer's, and a human merge is the review record.

Refs: PR #6976 · ui#6880 · ui#6208 · objectstack#13052. The objectstack twin
(objectstack#13663) covers that repo's hooks and is not addressed here.

Generated by Claude Code


Generated by Claude Code

…diation text
`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh`
each end their block message with the worktree recipe to run instead, and both created
the branch with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and
`AGENTS.md`; the hooks' copy was left behind — and it is the higher-leverage copy,
delivered at the exact moment an agent is about to act, so it is the one most likely to
be run verbatim.
The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
worktree of a repo shares. That write can fail after the branch is created, leaving a
branch with no worktree — a half state the error text does not name. Read as "the
worktree exists", the agent starts editing the shared primary checkout, which is the one
thing these two guards exist to prevent.
Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
(`git fetch origin main && … origin/main`, landed for ui#6208), so the missing flag was
the only surviving divergence from the prescribed form. One word added per file, net ±0
lines in both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 12:36
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit e55fdaeAug 31, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6977-hook-no-track branch August 31, 2026 13:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@huangyiirene@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(hooks): print the --no-track worktree recipe in both guards' remediation text - #7000

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track
Aug 31, 2026
Merged

fix(hooks): print the --no-track worktree recipe in both guards' remediation text#7000
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6977

What changed

Both worktree guards print a remediation recipe when they block — the copy an agent reads
at the exact moment it is about to act, and therefore the copy most likely to be run
verbatim. Both printed the recipe with a plain -b. PR #6976 hardened the prescribed form
in CLAUDE.md and AGENTS.md with --no-track; the hooks' copy was left behind.

One word added per file, net ±0 lines in both:

  • .claude/hooks/guard-main-checkout.sh:57
  • .claude/hooks/guard-main-checkout-bash.sh:544

Both now emit exactly the prescribed form (verbatim from a live invocation of each hook,
with $name interpolated):

 git fetch origin main && git worktree add --no-track ../objectui-TASK -b BRANCH origin/main
cd ../objectui-TASK && pnpm install # then re-run your edits there

Why

The hazard is the one PR #6976 established: plain -b writes the new branch's upstream
keys (branch.NAME.remote, branch.NAME.merge) into the one .git/config that every
linked worktree of a repo shares. That write can fail after the branch is created,
leaving a branch with no worktree — a half state the error text does not name. Read as
"the worktree exists", the agent starts editing the shared primary checkout, which is the
one thing these two guards exist to prevent.

Scope — additive, not a recipe rewrite

The premise was re-verified on origin/main before the first edit and it is narrower than
the sibling card's. This repo's hooks already emitted the fetch-hardened base
(git fetch origin main && … origin/main, landed for ui#6208), so the missing flag was the
only surviving divergence from the prescribed form. Measured on the freshly fetched tree:
grep -rc "no-track" .claude/hooks/ returned 0 across all seven files there (four guards,
three self-test matrices) before this change, and 1 in each of the two edited guards after.

Not in this PR

Each of these was examined and deliberately left alone:

  • .changeset/6208-worktree-recipe-fetch-base.md quotes the pre-ui#6208 recipe as the
    record of a past change. Editing a historical changeset rewrites that record —
    adjudicated in PR docs: harden the worktree recipe with --no-track; correct the stale no-mechanical-backstop claim #6976, and restated on the card. check-changeset-overwrite.mjs
    confirms it mechanically: 0 modified, 0 deleted.
  • The two -cmp comparison-tree lines in CLAUDE.md / AGENTS.md create no branch and
    take an explicit ref from the caller, so there are no upstream keys to write and the flag
    does not apply. This is the same reading ui#6208 recorded for them.
  • .claude/hooks/guard-tree-enum.selftest.sh:99 is a command payload fed to a different
    guard, asserting that ordinary commands pass through it. It is test input, not text any
    agent is told to run, and adding the flag there would change no coverage.

Verification

All at efbfb15, the head of this branch. Nine checks, joined with && so one verdict
covers all of them — exit 0.

Hook self-tests — the only gate that targets .claude/hooks/**
(.github/workflows/hook-selftests.yml), all three matrices it runs:

self-testresult
guard-main-checkout-bash.selftest.sh121 passed, 0 failed
guard-shared-stash.selftest.sh41 passed, 0 failed
guard-tree-enum.selftest.sh36 passed, 0 failed

Identical to the pre-edit baseline, and that is the expected direction rather than a weak
result: no self-test case pins the remediation string, so none needed updating in this
change. Measured, not assumed — a grep -rn for four distinctive fragments of the two block
messages (fetch origin main, dedicated worktree, Blocked: editing on the shared,
WRITES into the shared) across .claude/hooks/*.selftest.sh and scripts/ returns no
match in any self-test.

A second measurement, reported because its absence is the answer rather than an assumption:
this repo ships noguard-main-checkout.selftest.sh. .claude/hooks/ holds four guards
and three matrices; guard-main-checkout.sh is the one guard without one — so for that hook
the direct invocation below is the whole of its coverage.

Behavioural check — both hooks invoked directly with a payload targeting the shared
primary checkout. Each exits 2 (blocked, as designed) and prints the hardened recipe shown
above. The recipe it now prints was then fed back through all three of this repo's Bash
guards (guard-main-checkout-bash, guard-shared-stash, guard-tree-enum): all three exit
0, so the text is runnable and not self-blocked.

Repo gates that reach this diffcheck-control-bytes (5809 tracked text files
scanned), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite,
check-shell-escape-residue, check-governed-queue-guard --self-test (132 cases). All ✅.
bash -n clean on both edited files.

Measurement — nothing lints shell files' contents in this repo. Verified on this tree
rather than taken from the workflow header that claims it: eslint.config.js scopes every
block to **/*.{ts,tsx}, and there is no shellcheck step anywhere (the three matches in
the repo are two prose mentions and one # shellcheck source= directive inside
e2e/live/ci/start-backend.sh). check-shell-escape-residue reads fenced blocks in
AGENTS.md, CLAUDE.md, skills/ and content/docs/ — it does not reach .claude/hooks/.
So the self-test matrices plus the direct invocation above are the whole of the coverage
available for this diff, by measurement.

Changeset

check-changeset-presence reports no changeset is owed here (3 files changed, 0 of them
published source of a released package). One is included anyway, with empty frontmatter,
because that is this repo's verified convention for a diff of this shape — the same form as
.changeset/6880-worktree-recipe-no-track.md (PR #6976), 6208-worktree-recipe-fetch-base.md
and 6089-hook-selftests-step-names-drop-counts.md. The empty-frontmatter exemption is a
first-class pass in scripts/check-changeset-presence.mjs.

No skip-changeset label is applied: in this repo that label is a phantom.
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins that nothing under .github/ or
scripts/ may mention it, so applying it would exempt nothing.

Governed surface — draft, human merge

.claude/** is governed. check-governed-queue-guard --test on this diff returns
⛔ GOVERNED — 2 of 3 path(s), and one governed path governs the whole pull request. This PR
stays a draft: not flipped ready, not enqueued, no auto-merge armed. The merge is the
maintainer's, and a human merge is the review record.

Refs: PR #6976 · ui#6880 · ui#6208 · objectstack#13052. The objectstack twin
(objectstack#13663) covers that repo's hooks and is not addressed here.

Generated by Claude Code


Generated by Claude Code

…diation text
`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh`
each end their block message with the worktree recipe to run instead, and both created
the branch with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and
`AGENTS.md`; the hooks' copy was left behind — and it is the higher-leverage copy,
delivered at the exact moment an agent is about to act, so it is the one most likely to
be run verbatim.
The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
worktree of a repo shares. That write can fail after the branch is created, leaving a
branch with no worktree — a half state the error text does not name. Read as "the
worktree exists", the agent starts editing the shared primary checkout, which is the one
thing these two guards exist to prevent.
Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
(`git fetch origin main && … origin/main`, landed for ui#6208), so the missing flag was
the only surviving divergence from the prescribed form. One word added per file, net ±0
lines in both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 12:36
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit e55fdaeAug 31, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6977-hook-no-track branch August 31, 2026 13:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@huangyiirene@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(hooks): print the --no-track worktree recipe in both guards' remediation text - #7000

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track
Aug 31, 2026
Merged

fix(hooks): print the --no-track worktree recipe in both guards' remediation text#7000
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6977

What changed

Both worktree guards print a remediation recipe when they block — the copy an agent reads
at the exact moment it is about to act, and therefore the copy most likely to be run
verbatim. Both printed the recipe with a plain -b. PR #6976 hardened the prescribed form
in CLAUDE.md and AGENTS.md with --no-track; the hooks' copy was left behind.

One word added per file, net ±0 lines in both:

  • .claude/hooks/guard-main-checkout.sh:57
  • .claude/hooks/guard-main-checkout-bash.sh:544

Both now emit exactly the prescribed form (verbatim from a live invocation of each hook,
with $name interpolated):

 git fetch origin main && git worktree add --no-track ../objectui-TASK -b BRANCH origin/main
cd ../objectui-TASK && pnpm install # then re-run your edits there

Why

The hazard is the one PR #6976 established: plain -b writes the new branch's upstream
keys (branch.NAME.remote, branch.NAME.merge) into the one .git/config that every
linked worktree of a repo shares. That write can fail after the branch is created,
leaving a branch with no worktree — a half state the error text does not name. Read as
"the worktree exists", the agent starts editing the shared primary checkout, which is the
one thing these two guards exist to prevent.

Scope — additive, not a recipe rewrite

The premise was re-verified on origin/main before the first edit and it is narrower than
the sibling card's. This repo's hooks already emitted the fetch-hardened base
(git fetch origin main && … origin/main, landed for ui#6208), so the missing flag was the
only surviving divergence from the prescribed form. Measured on the freshly fetched tree:
grep -rc "no-track" .claude/hooks/ returned 0 across all seven files there (four guards,
three self-test matrices) before this change, and 1 in each of the two edited guards after.

Not in this PR

Each of these was examined and deliberately left alone:

  • .changeset/6208-worktree-recipe-fetch-base.md quotes the pre-ui#6208 recipe as the
    record of a past change. Editing a historical changeset rewrites that record —
    adjudicated in PR docs: harden the worktree recipe with --no-track; correct the stale no-mechanical-backstop claim #6976, and restated on the card. check-changeset-overwrite.mjs
    confirms it mechanically: 0 modified, 0 deleted.
  • The two -cmp comparison-tree lines in CLAUDE.md / AGENTS.md create no branch and
    take an explicit ref from the caller, so there are no upstream keys to write and the flag
    does not apply. This is the same reading ui#6208 recorded for them.
  • .claude/hooks/guard-tree-enum.selftest.sh:99 is a command payload fed to a different
    guard, asserting that ordinary commands pass through it. It is test input, not text any
    agent is told to run, and adding the flag there would change no coverage.

Verification

All at efbfb15, the head of this branch. Nine checks, joined with && so one verdict
covers all of them — exit 0.

Hook self-tests — the only gate that targets .claude/hooks/**
(.github/workflows/hook-selftests.yml), all three matrices it runs:

self-testresult
guard-main-checkout-bash.selftest.sh121 passed, 0 failed
guard-shared-stash.selftest.sh41 passed, 0 failed
guard-tree-enum.selftest.sh36 passed, 0 failed

Identical to the pre-edit baseline, and that is the expected direction rather than a weak
result: no self-test case pins the remediation string, so none needed updating in this
change. Measured, not assumed — a grep -rn for four distinctive fragments of the two block
messages (fetch origin main, dedicated worktree, Blocked: editing on the shared,
WRITES into the shared) across .claude/hooks/*.selftest.sh and scripts/ returns no
match in any self-test.

A second measurement, reported because its absence is the answer rather than an assumption:
this repo ships noguard-main-checkout.selftest.sh. .claude/hooks/ holds four guards
and three matrices; guard-main-checkout.sh is the one guard without one — so for that hook
the direct invocation below is the whole of its coverage.

Behavioural check — both hooks invoked directly with a payload targeting the shared
primary checkout. Each exits 2 (blocked, as designed) and prints the hardened recipe shown
above. The recipe it now prints was then fed back through all three of this repo's Bash
guards (guard-main-checkout-bash, guard-shared-stash, guard-tree-enum): all three exit
0, so the text is runnable and not self-blocked.

Repo gates that reach this diffcheck-control-bytes (5809 tracked text files
scanned), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite,
check-shell-escape-residue, check-governed-queue-guard --self-test (132 cases). All ✅.
bash -n clean on both edited files.

Measurement — nothing lints shell files' contents in this repo. Verified on this tree
rather than taken from the workflow header that claims it: eslint.config.js scopes every
block to **/*.{ts,tsx}, and there is no shellcheck step anywhere (the three matches in
the repo are two prose mentions and one # shellcheck source= directive inside
e2e/live/ci/start-backend.sh). check-shell-escape-residue reads fenced blocks in
AGENTS.md, CLAUDE.md, skills/ and content/docs/ — it does not reach .claude/hooks/.
So the self-test matrices plus the direct invocation above are the whole of the coverage
available for this diff, by measurement.

Changeset

check-changeset-presence reports no changeset is owed here (3 files changed, 0 of them
published source of a released package). One is included anyway, with empty frontmatter,
because that is this repo's verified convention for a diff of this shape — the same form as
.changeset/6880-worktree-recipe-no-track.md (PR #6976), 6208-worktree-recipe-fetch-base.md
and 6089-hook-selftests-step-names-drop-counts.md. The empty-frontmatter exemption is a
first-class pass in scripts/check-changeset-presence.mjs.

No skip-changeset label is applied: in this repo that label is a phantom.
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins that nothing under .github/ or
scripts/ may mention it, so applying it would exempt nothing.

Governed surface — draft, human merge

.claude/** is governed. check-governed-queue-guard --test on this diff returns
⛔ GOVERNED — 2 of 3 path(s), and one governed path governs the whole pull request. This PR
stays a draft: not flipped ready, not enqueued, no auto-merge armed. The merge is the
maintainer's, and a human merge is the review record.

Refs: PR #6976 · ui#6880 · ui#6208 · objectstack#13052. The objectstack twin
(objectstack#13663) covers that repo's hooks and is not addressed here.

Generated by Claude Code


Generated by Claude Code

…diation text
`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh`
each end their block message with the worktree recipe to run instead, and both created
the branch with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and
`AGENTS.md`; the hooks' copy was left behind — and it is the higher-leverage copy,
delivered at the exact moment an agent is about to act, so it is the one most likely to
be run verbatim.
The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
worktree of a repo shares. That write can fail after the branch is created, leaving a
branch with no worktree — a half state the error text does not name. Read as "the
worktree exists", the agent starts editing the shared primary checkout, which is the one
thing these two guards exist to prevent.
Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
(`git fetch origin main && … origin/main`, landed for ui#6208), so the missing flag was
the only surviving divergence from the prescribed form. One word added per file, net ±0
lines in both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 12:36
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit e55fdaeAug 31, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6977-hook-no-track branch August 31, 2026 13:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@huangyiirene@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(hooks): print the --no-track worktree recipe in both guards' remediation text - #7000

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track
Aug 31, 2026
Merged

fix(hooks): print the --no-track worktree recipe in both guards' remediation text#7000
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6977

What changed

Both worktree guards print a remediation recipe when they block — the copy an agent reads
at the exact moment it is about to act, and therefore the copy most likely to be run
verbatim. Both printed the recipe with a plain -b. PR #6976 hardened the prescribed form
in CLAUDE.md and AGENTS.md with --no-track; the hooks' copy was left behind.

One word added per file, net ±0 lines in both:

  • .claude/hooks/guard-main-checkout.sh:57
  • .claude/hooks/guard-main-checkout-bash.sh:544

Both now emit exactly the prescribed form (verbatim from a live invocation of each hook,
with $name interpolated):

 git fetch origin main && git worktree add --no-track ../objectui-TASK -b BRANCH origin/main
cd ../objectui-TASK && pnpm install # then re-run your edits there

Why

The hazard is the one PR #6976 established: plain -b writes the new branch's upstream
keys (branch.NAME.remote, branch.NAME.merge) into the one .git/config that every
linked worktree of a repo shares. That write can fail after the branch is created,
leaving a branch with no worktree — a half state the error text does not name. Read as
"the worktree exists", the agent starts editing the shared primary checkout, which is the
one thing these two guards exist to prevent.

Scope — additive, not a recipe rewrite

The premise was re-verified on origin/main before the first edit and it is narrower than
the sibling card's. This repo's hooks already emitted the fetch-hardened base
(git fetch origin main && … origin/main, landed for ui#6208), so the missing flag was the
only surviving divergence from the prescribed form. Measured on the freshly fetched tree:
grep -rc "no-track" .claude/hooks/ returned 0 across all seven files there (four guards,
three self-test matrices) before this change, and 1 in each of the two edited guards after.

Not in this PR

Each of these was examined and deliberately left alone:

  • .changeset/6208-worktree-recipe-fetch-base.md quotes the pre-ui#6208 recipe as the
    record of a past change. Editing a historical changeset rewrites that record —
    adjudicated in PR docs: harden the worktree recipe with --no-track; correct the stale no-mechanical-backstop claim #6976, and restated on the card. check-changeset-overwrite.mjs
    confirms it mechanically: 0 modified, 0 deleted.
  • The two -cmp comparison-tree lines in CLAUDE.md / AGENTS.md create no branch and
    take an explicit ref from the caller, so there are no upstream keys to write and the flag
    does not apply. This is the same reading ui#6208 recorded for them.
  • .claude/hooks/guard-tree-enum.selftest.sh:99 is a command payload fed to a different
    guard, asserting that ordinary commands pass through it. It is test input, not text any
    agent is told to run, and adding the flag there would change no coverage.

Verification

All at efbfb15, the head of this branch. Nine checks, joined with && so one verdict
covers all of them — exit 0.

Hook self-tests — the only gate that targets .claude/hooks/**
(.github/workflows/hook-selftests.yml), all three matrices it runs:

self-testresult
guard-main-checkout-bash.selftest.sh121 passed, 0 failed
guard-shared-stash.selftest.sh41 passed, 0 failed
guard-tree-enum.selftest.sh36 passed, 0 failed

Identical to the pre-edit baseline, and that is the expected direction rather than a weak
result: no self-test case pins the remediation string, so none needed updating in this
change. Measured, not assumed — a grep -rn for four distinctive fragments of the two block
messages (fetch origin main, dedicated worktree, Blocked: editing on the shared,
WRITES into the shared) across .claude/hooks/*.selftest.sh and scripts/ returns no
match in any self-test.

A second measurement, reported because its absence is the answer rather than an assumption:
this repo ships noguard-main-checkout.selftest.sh. .claude/hooks/ holds four guards
and three matrices; guard-main-checkout.sh is the one guard without one — so for that hook
the direct invocation below is the whole of its coverage.

Behavioural check — both hooks invoked directly with a payload targeting the shared
primary checkout. Each exits 2 (blocked, as designed) and prints the hardened recipe shown
above. The recipe it now prints was then fed back through all three of this repo's Bash
guards (guard-main-checkout-bash, guard-shared-stash, guard-tree-enum): all three exit
0, so the text is runnable and not self-blocked.

Repo gates that reach this diffcheck-control-bytes (5809 tracked text files
scanned), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite,
check-shell-escape-residue, check-governed-queue-guard --self-test (132 cases). All ✅.
bash -n clean on both edited files.

Measurement — nothing lints shell files' contents in this repo. Verified on this tree
rather than taken from the workflow header that claims it: eslint.config.js scopes every
block to **/*.{ts,tsx}, and there is no shellcheck step anywhere (the three matches in
the repo are two prose mentions and one # shellcheck source= directive inside
e2e/live/ci/start-backend.sh). check-shell-escape-residue reads fenced blocks in
AGENTS.md, CLAUDE.md, skills/ and content/docs/ — it does not reach .claude/hooks/.
So the self-test matrices plus the direct invocation above are the whole of the coverage
available for this diff, by measurement.

Changeset

check-changeset-presence reports no changeset is owed here (3 files changed, 0 of them
published source of a released package). One is included anyway, with empty frontmatter,
because that is this repo's verified convention for a diff of this shape — the same form as
.changeset/6880-worktree-recipe-no-track.md (PR #6976), 6208-worktree-recipe-fetch-base.md
and 6089-hook-selftests-step-names-drop-counts.md. The empty-frontmatter exemption is a
first-class pass in scripts/check-changeset-presence.mjs.

No skip-changeset label is applied: in this repo that label is a phantom.
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins that nothing under .github/ or
scripts/ may mention it, so applying it would exempt nothing.

Governed surface — draft, human merge

.claude/** is governed. check-governed-queue-guard --test on this diff returns
⛔ GOVERNED — 2 of 3 path(s), and one governed path governs the whole pull request. This PR
stays a draft: not flipped ready, not enqueued, no auto-merge armed. The merge is the
maintainer's, and a human merge is the review record.

Refs: PR #6976 · ui#6880 · ui#6208 · objectstack#13052. The objectstack twin
(objectstack#13663) covers that repo's hooks and is not addressed here.

Generated by Claude Code


Generated by Claude Code

…diation text
`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh`
each end their block message with the worktree recipe to run instead, and both created
the branch with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and
`AGENTS.md`; the hooks' copy was left behind — and it is the higher-leverage copy,
delivered at the exact moment an agent is about to act, so it is the one most likely to
be run verbatim.
The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
worktree of a repo shares. That write can fail after the branch is created, leaving a
branch with no worktree — a half state the error text does not name. Read as "the
worktree exists", the agent starts editing the shared primary checkout, which is the one
thing these two guards exist to prevent.
Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
(`git fetch origin main && … origin/main`, landed for ui#6208), so the missing flag was
the only surviving divergence from the prescribed form. One word added per file, net ±0
lines in both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 12:36
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit e55fdaeAug 31, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6977-hook-no-track branch August 31, 2026 13:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@huangyiirene@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(hooks): print the --no-track worktree recipe in both guards' remediation text - #7000

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track
Aug 31, 2026
Merged

fix(hooks): print the --no-track worktree recipe in both guards' remediation text#7000
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6977

What changed

Both worktree guards print a remediation recipe when they block — the copy an agent reads
at the exact moment it is about to act, and therefore the copy most likely to be run
verbatim. Both printed the recipe with a plain -b. PR #6976 hardened the prescribed form
in CLAUDE.md and AGENTS.md with --no-track; the hooks' copy was left behind.

One word added per file, net ±0 lines in both:

  • .claude/hooks/guard-main-checkout.sh:57
  • .claude/hooks/guard-main-checkout-bash.sh:544

Both now emit exactly the prescribed form (verbatim from a live invocation of each hook,
with $name interpolated):

 git fetch origin main && git worktree add --no-track ../objectui-TASK -b BRANCH origin/main
cd ../objectui-TASK && pnpm install # then re-run your edits there

Why

The hazard is the one PR #6976 established: plain -b writes the new branch's upstream
keys (branch.NAME.remote, branch.NAME.merge) into the one .git/config that every
linked worktree of a repo shares. That write can fail after the branch is created,
leaving a branch with no worktree — a half state the error text does not name. Read as
"the worktree exists", the agent starts editing the shared primary checkout, which is the
one thing these two guards exist to prevent.

Scope — additive, not a recipe rewrite

The premise was re-verified on origin/main before the first edit and it is narrower than
the sibling card's. This repo's hooks already emitted the fetch-hardened base
(git fetch origin main && … origin/main, landed for ui#6208), so the missing flag was the
only surviving divergence from the prescribed form. Measured on the freshly fetched tree:
grep -rc "no-track" .claude/hooks/ returned 0 across all seven files there (four guards,
three self-test matrices) before this change, and 1 in each of the two edited guards after.

Not in this PR

Each of these was examined and deliberately left alone:

  • .changeset/6208-worktree-recipe-fetch-base.md quotes the pre-ui#6208 recipe as the
    record of a past change. Editing a historical changeset rewrites that record —
    adjudicated in PR docs: harden the worktree recipe with --no-track; correct the stale no-mechanical-backstop claim #6976, and restated on the card. check-changeset-overwrite.mjs
    confirms it mechanically: 0 modified, 0 deleted.
  • The two -cmp comparison-tree lines in CLAUDE.md / AGENTS.md create no branch and
    take an explicit ref from the caller, so there are no upstream keys to write and the flag
    does not apply. This is the same reading ui#6208 recorded for them.
  • .claude/hooks/guard-tree-enum.selftest.sh:99 is a command payload fed to a different
    guard, asserting that ordinary commands pass through it. It is test input, not text any
    agent is told to run, and adding the flag there would change no coverage.

Verification

All at efbfb15, the head of this branch. Nine checks, joined with && so one verdict
covers all of them — exit 0.

Hook self-tests — the only gate that targets .claude/hooks/**
(.github/workflows/hook-selftests.yml), all three matrices it runs:

self-testresult
guard-main-checkout-bash.selftest.sh121 passed, 0 failed
guard-shared-stash.selftest.sh41 passed, 0 failed
guard-tree-enum.selftest.sh36 passed, 0 failed

Identical to the pre-edit baseline, and that is the expected direction rather than a weak
result: no self-test case pins the remediation string, so none needed updating in this
change. Measured, not assumed — a grep -rn for four distinctive fragments of the two block
messages (fetch origin main, dedicated worktree, Blocked: editing on the shared,
WRITES into the shared) across .claude/hooks/*.selftest.sh and scripts/ returns no
match in any self-test.

A second measurement, reported because its absence is the answer rather than an assumption:
this repo ships noguard-main-checkout.selftest.sh. .claude/hooks/ holds four guards
and three matrices; guard-main-checkout.sh is the one guard without one — so for that hook
the direct invocation below is the whole of its coverage.

Behavioural check — both hooks invoked directly with a payload targeting the shared
primary checkout. Each exits 2 (blocked, as designed) and prints the hardened recipe shown
above. The recipe it now prints was then fed back through all three of this repo's Bash
guards (guard-main-checkout-bash, guard-shared-stash, guard-tree-enum): all three exit
0, so the text is runnable and not self-blocked.

Repo gates that reach this diffcheck-control-bytes (5809 tracked text files
scanned), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite,
check-shell-escape-residue, check-governed-queue-guard --self-test (132 cases). All ✅.
bash -n clean on both edited files.

Measurement — nothing lints shell files' contents in this repo. Verified on this tree
rather than taken from the workflow header that claims it: eslint.config.js scopes every
block to **/*.{ts,tsx}, and there is no shellcheck step anywhere (the three matches in
the repo are two prose mentions and one # shellcheck source= directive inside
e2e/live/ci/start-backend.sh). check-shell-escape-residue reads fenced blocks in
AGENTS.md, CLAUDE.md, skills/ and content/docs/ — it does not reach .claude/hooks/.
So the self-test matrices plus the direct invocation above are the whole of the coverage
available for this diff, by measurement.

Changeset

check-changeset-presence reports no changeset is owed here (3 files changed, 0 of them
published source of a released package). One is included anyway, with empty frontmatter,
because that is this repo's verified convention for a diff of this shape — the same form as
.changeset/6880-worktree-recipe-no-track.md (PR #6976), 6208-worktree-recipe-fetch-base.md
and 6089-hook-selftests-step-names-drop-counts.md. The empty-frontmatter exemption is a
first-class pass in scripts/check-changeset-presence.mjs.

No skip-changeset label is applied: in this repo that label is a phantom.
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins that nothing under .github/ or
scripts/ may mention it, so applying it would exempt nothing.

Governed surface — draft, human merge

.claude/** is governed. check-governed-queue-guard --test on this diff returns
⛔ GOVERNED — 2 of 3 path(s), and one governed path governs the whole pull request. This PR
stays a draft: not flipped ready, not enqueued, no auto-merge armed. The merge is the
maintainer's, and a human merge is the review record.

Refs: PR #6976 · ui#6880 · ui#6208 · objectstack#13052. The objectstack twin
(objectstack#13663) covers that repo's hooks and is not addressed here.

Generated by Claude Code


Generated by Claude Code

…diation text
`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh`
each end their block message with the worktree recipe to run instead, and both created
the branch with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and
`AGENTS.md`; the hooks' copy was left behind — and it is the higher-leverage copy,
delivered at the exact moment an agent is about to act, so it is the one most likely to
be run verbatim.
The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
worktree of a repo shares. That write can fail after the branch is created, leaving a
branch with no worktree — a half state the error text does not name. Read as "the
worktree exists", the agent starts editing the shared primary checkout, which is the one
thing these two guards exist to prevent.
Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
(`git fetch origin main && … origin/main`, landed for ui#6208), so the missing flag was
the only surviving divergence from the prescribed form. One word added per file, net ±0
lines in both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 12:36
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit e55fdaeAug 31, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6977-hook-no-track branch August 31, 2026 13:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@huangyiirene@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(hooks): print the --no-track worktree recipe in both guards' remediation text - #7000

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track
Aug 31, 2026
Merged

fix(hooks): print the --no-track worktree recipe in both guards' remediation text#7000
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6977

What changed

Both worktree guards print a remediation recipe when they block — the copy an agent reads
at the exact moment it is about to act, and therefore the copy most likely to be run
verbatim. Both printed the recipe with a plain -b. PR #6976 hardened the prescribed form
in CLAUDE.md and AGENTS.md with --no-track; the hooks' copy was left behind.

One word added per file, net ±0 lines in both:

  • .claude/hooks/guard-main-checkout.sh:57
  • .claude/hooks/guard-main-checkout-bash.sh:544

Both now emit exactly the prescribed form (verbatim from a live invocation of each hook,
with $name interpolated):

 git fetch origin main && git worktree add --no-track ../objectui-TASK -b BRANCH origin/main
cd ../objectui-TASK && pnpm install # then re-run your edits there

Why

The hazard is the one PR #6976 established: plain -b writes the new branch's upstream
keys (branch.NAME.remote, branch.NAME.merge) into the one .git/config that every
linked worktree of a repo shares. That write can fail after the branch is created,
leaving a branch with no worktree — a half state the error text does not name. Read as
"the worktree exists", the agent starts editing the shared primary checkout, which is the
one thing these two guards exist to prevent.

Scope — additive, not a recipe rewrite

The premise was re-verified on origin/main before the first edit and it is narrower than
the sibling card's. This repo's hooks already emitted the fetch-hardened base
(git fetch origin main && … origin/main, landed for ui#6208), so the missing flag was the
only surviving divergence from the prescribed form. Measured on the freshly fetched tree:
grep -rc "no-track" .claude/hooks/ returned 0 across all seven files there (four guards,
three self-test matrices) before this change, and 1 in each of the two edited guards after.

Not in this PR

Each of these was examined and deliberately left alone:

  • .changeset/6208-worktree-recipe-fetch-base.md quotes the pre-ui#6208 recipe as the
    record of a past change. Editing a historical changeset rewrites that record —
    adjudicated in PR docs: harden the worktree recipe with --no-track; correct the stale no-mechanical-backstop claim #6976, and restated on the card. check-changeset-overwrite.mjs
    confirms it mechanically: 0 modified, 0 deleted.
  • The two -cmp comparison-tree lines in CLAUDE.md / AGENTS.md create no branch and
    take an explicit ref from the caller, so there are no upstream keys to write and the flag
    does not apply. This is the same reading ui#6208 recorded for them.
  • .claude/hooks/guard-tree-enum.selftest.sh:99 is a command payload fed to a different
    guard, asserting that ordinary commands pass through it. It is test input, not text any
    agent is told to run, and adding the flag there would change no coverage.

Verification

All at efbfb15, the head of this branch. Nine checks, joined with && so one verdict
covers all of them — exit 0.

Hook self-tests — the only gate that targets .claude/hooks/**
(.github/workflows/hook-selftests.yml), all three matrices it runs:

self-testresult
guard-main-checkout-bash.selftest.sh121 passed, 0 failed
guard-shared-stash.selftest.sh41 passed, 0 failed
guard-tree-enum.selftest.sh36 passed, 0 failed

Identical to the pre-edit baseline, and that is the expected direction rather than a weak
result: no self-test case pins the remediation string, so none needed updating in this
change. Measured, not assumed — a grep -rn for four distinctive fragments of the two block
messages (fetch origin main, dedicated worktree, Blocked: editing on the shared,
WRITES into the shared) across .claude/hooks/*.selftest.sh and scripts/ returns no
match in any self-test.

A second measurement, reported because its absence is the answer rather than an assumption:
this repo ships noguard-main-checkout.selftest.sh. .claude/hooks/ holds four guards
and three matrices; guard-main-checkout.sh is the one guard without one — so for that hook
the direct invocation below is the whole of its coverage.

Behavioural check — both hooks invoked directly with a payload targeting the shared
primary checkout. Each exits 2 (blocked, as designed) and prints the hardened recipe shown
above. The recipe it now prints was then fed back through all three of this repo's Bash
guards (guard-main-checkout-bash, guard-shared-stash, guard-tree-enum): all three exit
0, so the text is runnable and not self-blocked.

Repo gates that reach this diffcheck-control-bytes (5809 tracked text files
scanned), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite,
check-shell-escape-residue, check-governed-queue-guard --self-test (132 cases). All ✅.
bash -n clean on both edited files.

Measurement — nothing lints shell files' contents in this repo. Verified on this tree
rather than taken from the workflow header that claims it: eslint.config.js scopes every
block to **/*.{ts,tsx}, and there is no shellcheck step anywhere (the three matches in
the repo are two prose mentions and one # shellcheck source= directive inside
e2e/live/ci/start-backend.sh). check-shell-escape-residue reads fenced blocks in
AGENTS.md, CLAUDE.md, skills/ and content/docs/ — it does not reach .claude/hooks/.
So the self-test matrices plus the direct invocation above are the whole of the coverage
available for this diff, by measurement.

Changeset

check-changeset-presence reports no changeset is owed here (3 files changed, 0 of them
published source of a released package). One is included anyway, with empty frontmatter,
because that is this repo's verified convention for a diff of this shape — the same form as
.changeset/6880-worktree-recipe-no-track.md (PR #6976), 6208-worktree-recipe-fetch-base.md
and 6089-hook-selftests-step-names-drop-counts.md. The empty-frontmatter exemption is a
first-class pass in scripts/check-changeset-presence.mjs.

No skip-changeset label is applied: in this repo that label is a phantom.
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins that nothing under .github/ or
scripts/ may mention it, so applying it would exempt nothing.

Governed surface — draft, human merge

.claude/** is governed. check-governed-queue-guard --test on this diff returns
⛔ GOVERNED — 2 of 3 path(s), and one governed path governs the whole pull request. This PR
stays a draft: not flipped ready, not enqueued, no auto-merge armed. The merge is the
maintainer's, and a human merge is the review record.

Refs: PR #6976 · ui#6880 · ui#6208 · objectstack#13052. The objectstack twin
(objectstack#13663) covers that repo's hooks and is not addressed here.

Generated by Claude Code


Generated by Claude Code

…diation text
`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh`
each end their block message with the worktree recipe to run instead, and both created
the branch with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and
`AGENTS.md`; the hooks' copy was left behind — and it is the higher-leverage copy,
delivered at the exact moment an agent is about to act, so it is the one most likely to
be run verbatim.
The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
worktree of a repo shares. That write can fail after the branch is created, leaving a
branch with no worktree — a half state the error text does not name. Read as "the
worktree exists", the agent starts editing the shared primary checkout, which is the one
thing these two guards exist to prevent.
Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
(`git fetch origin main && … origin/main`, landed for ui#6208), so the missing flag was
the only surviving divergence from the prescribed form. One word added per file, net ±0
lines in both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 12:36
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit e55fdaeAug 31, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6977-hook-no-track branch August 31, 2026 13:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@huangyiirene@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(hooks): print the --no-track worktree recipe in both guards' remediation text - #7000

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track
Aug 31, 2026
Merged

fix(hooks): print the --no-track worktree recipe in both guards' remediation text#7000
os-zhuang merged 1 commit into
mainfrom
claude/issue-6977-hook-no-track

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#6977

What changed

Both worktree guards print a remediation recipe when they block — the copy an agent reads
at the exact moment it is about to act, and therefore the copy most likely to be run
verbatim. Both printed the recipe with a plain -b. PR #6976 hardened the prescribed form
in CLAUDE.md and AGENTS.md with --no-track; the hooks' copy was left behind.

One word added per file, net ±0 lines in both:

  • .claude/hooks/guard-main-checkout.sh:57
  • .claude/hooks/guard-main-checkout-bash.sh:544

Both now emit exactly the prescribed form (verbatim from a live invocation of each hook,
with $name interpolated):

 git fetch origin main && git worktree add --no-track ../objectui-TASK -b BRANCH origin/main
cd ../objectui-TASK && pnpm install # then re-run your edits there

Why

The hazard is the one PR #6976 established: plain -b writes the new branch's upstream
keys (branch.NAME.remote, branch.NAME.merge) into the one .git/config that every
linked worktree of a repo shares. That write can fail after the branch is created,
leaving a branch with no worktree — a half state the error text does not name. Read as
"the worktree exists", the agent starts editing the shared primary checkout, which is the
one thing these two guards exist to prevent.

Scope — additive, not a recipe rewrite

The premise was re-verified on origin/main before the first edit and it is narrower than
the sibling card's. This repo's hooks already emitted the fetch-hardened base
(git fetch origin main && … origin/main, landed for ui#6208), so the missing flag was the
only surviving divergence from the prescribed form. Measured on the freshly fetched tree:
grep -rc "no-track" .claude/hooks/ returned 0 across all seven files there (four guards,
three self-test matrices) before this change, and 1 in each of the two edited guards after.

Not in this PR

Each of these was examined and deliberately left alone:

  • .changeset/6208-worktree-recipe-fetch-base.md quotes the pre-ui#6208 recipe as the
    record of a past change. Editing a historical changeset rewrites that record —
    adjudicated in PR docs: harden the worktree recipe with --no-track; correct the stale no-mechanical-backstop claim #6976, and restated on the card. check-changeset-overwrite.mjs
    confirms it mechanically: 0 modified, 0 deleted.
  • The two -cmp comparison-tree lines in CLAUDE.md / AGENTS.md create no branch and
    take an explicit ref from the caller, so there are no upstream keys to write and the flag
    does not apply. This is the same reading ui#6208 recorded for them.
  • .claude/hooks/guard-tree-enum.selftest.sh:99 is a command payload fed to a different
    guard, asserting that ordinary commands pass through it. It is test input, not text any
    agent is told to run, and adding the flag there would change no coverage.

Verification

All at efbfb15, the head of this branch. Nine checks, joined with && so one verdict
covers all of them — exit 0.

Hook self-tests — the only gate that targets .claude/hooks/**
(.github/workflows/hook-selftests.yml), all three matrices it runs:

self-testresult
guard-main-checkout-bash.selftest.sh121 passed, 0 failed
guard-shared-stash.selftest.sh41 passed, 0 failed
guard-tree-enum.selftest.sh36 passed, 0 failed

Identical to the pre-edit baseline, and that is the expected direction rather than a weak
result: no self-test case pins the remediation string, so none needed updating in this
change. Measured, not assumed — a grep -rn for four distinctive fragments of the two block
messages (fetch origin main, dedicated worktree, Blocked: editing on the shared,
WRITES into the shared) across .claude/hooks/*.selftest.sh and scripts/ returns no
match in any self-test.

A second measurement, reported because its absence is the answer rather than an assumption:
this repo ships noguard-main-checkout.selftest.sh. .claude/hooks/ holds four guards
and three matrices; guard-main-checkout.sh is the one guard without one — so for that hook
the direct invocation below is the whole of its coverage.

Behavioural check — both hooks invoked directly with a payload targeting the shared
primary checkout. Each exits 2 (blocked, as designed) and prints the hardened recipe shown
above. The recipe it now prints was then fed back through all three of this repo's Bash
guards (guard-main-checkout-bash, guard-shared-stash, guard-tree-enum): all three exit
0, so the text is runnable and not self-blocked.

Repo gates that reach this diffcheck-control-bytes (5809 tracked text files
scanned), check-changeset-presence, check-changeset-no-major, check-changeset-overwrite,
check-shell-escape-residue, check-governed-queue-guard --self-test (132 cases). All ✅.
bash -n clean on both edited files.

Measurement — nothing lints shell files' contents in this repo. Verified on this tree
rather than taken from the workflow header that claims it: eslint.config.js scopes every
block to **/*.{ts,tsx}, and there is no shellcheck step anywhere (the three matches in
the repo are two prose mentions and one # shellcheck source= directive inside
e2e/live/ci/start-backend.sh). check-shell-escape-residue reads fenced blocks in
AGENTS.md, CLAUDE.md, skills/ and content/docs/ — it does not reach .claude/hooks/.
So the self-test matrices plus the direct invocation above are the whole of the coverage
available for this diff, by measurement.

Changeset

check-changeset-presence reports no changeset is owed here (3 files changed, 0 of them
published source of a released package). One is included anyway, with empty frontmatter,
because that is this repo's verified convention for a diff of this shape — the same form as
.changeset/6880-worktree-recipe-no-track.md (PR #6976), 6208-worktree-recipe-fetch-base.md
and 6089-hook-selftests-step-names-drop-counts.md. The empty-frontmatter exemption is a
first-class pass in scripts/check-changeset-presence.mjs.

No skip-changeset label is applied: in this repo that label is a phantom.
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins that nothing under .github/ or
scripts/ may mention it, so applying it would exempt nothing.

Governed surface — draft, human merge

.claude/** is governed. check-governed-queue-guard --test on this diff returns
⛔ GOVERNED — 2 of 3 path(s), and one governed path governs the whole pull request. This PR
stays a draft: not flipped ready, not enqueued, no auto-merge armed. The merge is the
maintainer's, and a human merge is the review record.

Refs: PR #6976 · ui#6880 · ui#6208 · objectstack#13052. The objectstack twin
(objectstack#13663) covers that repo's hooks and is not addressed here.

Generated by Claude Code


Generated by Claude Code

…diation text
`.claude/hooks/guard-main-checkout.sh` and `.claude/hooks/guard-main-checkout-bash.sh`
each end their block message with the worktree recipe to run instead, and both created
the branch with a plain `-b`. PR #6976 hardened the prescribed form in `CLAUDE.md` and
`AGENTS.md`; the hooks' copy was left behind — and it is the higher-leverage copy,
delivered at the exact moment an agent is about to act, so it is the one most likely to
be run verbatim.
The hazard is the one #6976 established: plain `-b` writes the new branch's upstream keys
(`branch.NAME.remote`, `branch.NAME.merge`) into the one `.git/config` that every linked
worktree of a repo shares. That write can fail after the branch is created, leaving a
branch with no worktree — a half state the error text does not name. Read as "the
worktree exists", the agent starts editing the shared primary checkout, which is the one
thing these two guards exist to prevent.
Additive, not a recipe rewrite: this repo's hooks already emitted the fetch-hardened base
(`git fetch origin main && … origin/main`, landed for ui#6208), so the missing flag was
the only surviving divergence from the prescribed form. One word added per file, net ±0
lines in both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
@os-zhuang
os-zhuang marked this pull request as ready for review August 31, 2026 12:36
@os-zhuang
os-zhuang added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit e55fdaeAug 31, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6977-hook-no-track branch August 31, 2026 13:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@huangyiirene@os-zhuang@claude