Uh oh!
There was an error while loading. Please reload this page.
feat(hooks): guard the origin/main ENUMERATION half, and record the incident in AGENTS.md - #6908
Conversation
… incident here This repository is where the defect was measured on 2026-08-29. The "verify main with origin/main" rule covers reading file CONTENTS; it does not cover enumerating which files EXIST. A working-tree glob feeding per-file origin/main reads yields a zero that looks like a full-tree scan: 30 workflow files in the working tree vs 31 on origin/main, and the missing one was governed-surface-guard.yml, which declares ready_for_review — the answer to the very question being asked. 10 PRs were flipped to ready on that reading during a runner-capacity outage. - `.claude/hooks/guard-tree-enum.sh` (+ self-test) — mirrored from objectstack, verdict logic case-for-case identical so the two repos' guards cannot drift (193 executable lines on both sides; only the incident's framing and the self-test's example paths are localised). Blocks one command carrying BOTH a working-tree enumeration AND an `origin/...` read; either half alone, and any command enumerating with `git ls-tree ... origin/...`, is allowed. Fails OPEN on anything it cannot parse. `OS_ALLOW_TREE_ENUM=1` is the deliberate exception. - `AGENTS.md` — a new subsection extending the 阳性对照 rule directly above it, since that is the rule that failed: the zero-hit control that was run passed, because it was drawn from the same faulty file list, so it validated the matcher rather than the enumeration. - `.github/workflows/hook-selftests.yml` — the new self-test had to be added BY HAND, because the steps here are a hand-kept enumeration of `.claude/hooks/*.selftest.sh` rather than a discovery loop like objectstack's. Without this the mirrored guard would ship with its matrix uncalled. The underlying shape is filed separately rather than redesigned here. - `scripts/dependabot-merge-gate.mjs` — one word: its description of this workflow said "the two ... matrices", which the step above makes stale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
Uh oh!
There was an error while loading. Please reload this page.
zhuangjianguo
commented
Aug 31, 2026
Queue dequeue root-caused — not this PR's failure; re-queueing once. Failing check: merge-queue CI, Why it is not this PR's:
Actions: the race is filed as #6953 for the plugin-gantt owners (the fix belongs in that test, not in this PR — widening this PR into plugin-gantt would be scope creep on a governed-surface change); this PR is re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that re-confirms #6953, raises its priority, and this PR stays parked on that fix — escalated to the maintainer then. Generated by Claude Code |
Part of objectstack-ai/objectstack#13305 — the objectui half. The objectstack PR carries the closing keyword; one card, one closer. Draft on purpose: governed surface (
.claude/**,AGENTS.md), human merge.What was wrong
The
origin/mainreading rule covers file contents. It does not cover file enumeration. A sweep that obeys the first half and not the second produces a zero that looks like a full-tree scan — every file it opens is read correctly, and files present onorigin/mainbut absent at the working tree's HEAD are never iterated, so the loop cannot report them and cannot know it missed them.Measured in this repository on 2026-08-29:
ls .github/workflows/*.ymlpiped towc -l— 30git ls-tree --name-only origin/main .github/workflows/filtered to.yml— 31.github/workflows/governed-surface-guard.yml, whose line 36 declarestypes: [opened, synchronize, reopened, ready_for_review]The question being asked was "would flipping these drafts to ready trigger any CI?". The answer to it was the file left out of the enumeration. 10 PRs were flipped to ready on that reading, each going from
mergeable_state: cleantounstable, during a runner-capacity outage.The zero-hit control that was run did not catch it, and passed.
pull_requestmatched 5 workflow files, so the method demonstrably worked — but those 5 came from the same faulty list, so the control validated the matcher, not the enumeration.What this PR does — mechanical first, prose subordinate
1.
.claude/hooks/guard-tree-enum.sh(+ self-test). A PreToolUse guard blocking ONE command that carries both a working-tree enumeration (for NAME in GLOB,ls GLOB,find PATH) and anorigin/...content read (git show,git grep,git cat-file). Either half alone is ordinary and is allowed. A command enumerating withgit ls-tree ... origin/...is never blocked, however it then reads — the population/read cross-check must not be harder to write than the bug. Conservative: anything it cannot parse fails open, andOS_ALLOW_TREE_ENUM=1is the deliberate exception, following theOS_ALLOW_MAIN_EDITS/OS_ALLOW_STASHfamily. Registered in.claude/settings.jsonalongside the other two Bash-matcher guards.Mirrored from objectstack with the discipline
guard-shared-stash.shalready documents — verdict logic case-for-case identical so the two repos' guards cannot drift. Proven, not asserted: stripping comments and blanks leaves 193 executable lines on both sides, and the only differing line is message text inside the block heredoc.2.
AGENTS.md. A new subsection placed directly under the existing 阳性对照 rule, because that is the rule that failed. It carries the incident's numbers, the canonical idiom, and the transferable sentence verbatim:No line ratchet applies in this repo (checked, not assumed — this repo has no
check-skill-line-ratchetequivalent);AGENTS.mdgoes 448 to 489 lines.3.
.github/workflows/hook-selftests.yml— a declared surface expansion. Not in the dispatched file surface, and done anyway because skipping it would ship a self-test that nothing runs. This workflow's steps are a hand-kept enumeration of.claude/hooks/*.selftest.sh, so the new matrix would have been silently uncalled. objectstack's twin discovers withfindand needed no edit. The underlying shape is not redesigned here — filed as #6906 instead.4.
scripts/dependabot-merge-gate.mjs— one word. Its description of that workflow said "the two ... matrices", which step 3 makes stale. De-counted rather than re-counted, matching that file's own documented stance on hand-copied enumerations.Verification, at
0a30e47Reverse verification on the mirrored hook — neutering
is_glob_wordpredicted exactly 8 of 9 block cases flipping toallow, with thefindcase staying blocked because it does not consult that helper. Observed:28 passed, 8 failed, thefindcase absent from the failures. Mutation confirmed on disk by marker count and by a changed blob hash; restore proven byte-identical (same hash) and self-test back to36 passed, 0 failed.Gates, each exit code captured by redirect-then-capture, never through a pipe:
check:control-bytes,check:doc-fences,check:governed-queue-guard,check:skills-paths,check:shell-escape-residue,check-changeset-presence.mjs,check-doc-links.mjs— all exit 0guard-tree-enum selftest: 36 passed, 0 failedscripts/__tests__suites for the edited gate script and the touched workflows —Test Files 6 passed (6),Tests 143 passed (143)(dependabot-merge-gate,merge-queue-reporting,check-governed-queue-guard,ci-cd-pipeline-doc,lint-workflow,check-shell-escape-residue)No changeset:
check-changeset-presence.mjsexits 0 — no package source is touched. Theskip-changesetlabel is deliberately not applied: in this repository it is a phantom that no workflow or gate reads, andscripts/__tests__/ci-cd-pipeline-doc.test.tspins it that way.Dev session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
Generated by Claude Code