Skip to content

test(scripts): check-shell-escape-residue reds on an agent-tree document no scan root reaches (#7413) - #8010

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-7413-shell-escape-residue-claude-root
Sep 6, 2026
Merged

test(scripts): check-shell-escape-residue reds on an agent-tree document no scan root reaches (#7413)#8010
baozhoutao merged 1 commit into
mainfrom
claude/issue-7413-shell-escape-residue-claude-root

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #7413

Direction 2 of the card, which is the general form: the gate now judges whether anything in the agent tree is off its scan surface, as its own verdict — not a widened row.

The assertion's design

SCAN_ROOTS answers what is scanned. Nothing answered is anything in the agent tree NOT scanned, and no minFiles floor can: a floor detects a root that collapsed, never a document that was never on the surface. Three shapes, only one of which had a signal before this:

shape signal before
a declared root walks to zero minFiles fires (#7403's fix)
a declared root vanishes from disk unresolved fires
a new document outside every row nothing
a SCAN_ROOTS row is deleted nothing

COVERAGE_TREES adds the missing judgement: trees in which every .md/.mdx must be reached by some declared row, reported by file name when one is not.

It cannot disagree with the scan about what a document is. Coverage is set membership in the walk's own output — scan collects every path listDocuments returned for every resolved root, then asks the same listDocuments for the tree. One walk, one DOC_EXTENSIONS, one definition. A second glob or a prefix match on the row spec would be a second answer to one fact (#3261/#3279), and the drift would land on the side that reads as coverage.

An unresolvable coverage tree is loud for the reason a scan root is — a mistyped .claude covers nothing and reads as coverage forever — and it carries a role so the remedy names the right declaration list. The coverage figure rides in the verdict line, so a green still shows its population:

✅  check-shell-escape-residue: OK (… ; coverage -- .claude: 4/4; skills: 16/16 document(s) under a declared root).

The skills/-side measurement the card asked for

It generalises, at the cost of one extra walk of 16 files, so it is included.

⛔ No allowlist and no per-file opt-out: the two ways to clear a finding are to move the document under a declared root, or to declare a root for it — both the deliberate decision the gap deserves.

Reverse verification

On disk, against the real gate. .claude/agents/reviewer.md — the card's literal scenario — planted with a fenced bash block, inside a trap:

=== BEFORE: .claude md count = 4, reviewer.md present = no
=== AFTER plant: .claude md count = 5, reviewer.md present = yes
=== PLANTED gate EXIT=1

❌  check-shell-escape-residue: 1 agent-tree document is on no scan root

    - .claude/agents/reviewer.md (under the coverage tree `.claude`)

=== RESTORED: .claude md count = 4, agents dir present = no
=== RESTORED gate EXIT=0
=== git status --porcelain (must be empty):
(end)

Ablation of the implementation. Committed first, then the detection line mutated on disk (const missing = documents.filter(…)[]), verified landed by marker count and by a blob-hash mismatch against HEAD, then restored:

=== BEFORE: detection line present? 1
=== AFTER mutation: original line count = 0, marker count = 1
=== MUTATED vitest EXIT=1
      Tests  4 failed | 39 passed (43)
=== RESTORED: hash matches HEAD? YES
=== git diff HEAD (must be empty):
(end)

The four that go red are the coverage ablation, the row-deletion case, the same-walk case, and the pre-existing out-of-scope case whose fixture already contained a .claude/hooks/notes.md nobody was reporting. The ⭐ REDS on this card's own scenario case also asserts that every other signal in the same run reads healthy — no unresolved, no vacuous, .claude/skills still returning its file, the planted residue not even judged. Without that half the case would pass for a gate that reddened on anything.

Gates (all at 150ff53, exit codes captured by redirect-then-capture)

gate verdict
node scripts/check-shell-escape-residue.mjs exit 0 — OK (5/5 root(s) resolved …; coverage -- .claude: 4/4; skills: 16/16 …)
pnpm exec vitest run scripts/__tests__/check-shell-escape-residue.test.ts exit 0 — Test Files 1 passed (1) / Tests 43 passed (43)
pnpm exec vitest run scripts/__tests__/ exit 0 — Test Files 107 passed (107) / Tests 3253 passed (3253)
pnpm type-check:scripts exit 0
pnpm lint:root exit 0 — 32 problems (0 errors, 32 warnings), none in the two changed files
pnpm check:control-bytes exit 0 — scanned 6440 tracked text file(s); plus a direct control-byte grep over both files: clean
node scripts/check-changeset-presence.mjs exit 0 — No source or published contract of a released package changed in this range, so no changeset is owed
node scripts/check-governed-queue-guard.mjs --test THE_2_CHANGED_PATHS exit 0 — NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched

Readers

git grep -l check-shell-escape-residue -- scripts/ .github/ returns exactly three files: the gate, its test, and .github/workflows/shell-escape-residue.yml.

The workflow needs no .claude trigger path — it deliberately has no path filter at all. Its header: "Hence: no paths and no paths-ignore here, deliberately. scripts/__tests__/check-shell-escape-residue.test.ts fails if either is ever added" — and that pin (runs it in NO path-filtered workflow) is still green. It fires on every pull request shape, which is also what makes it requirable.

Note on CI

Live E2E (informational) is red on every branch today for an upstream reason (#7990 / objectstack#16186), unrelated to this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MM7kaS4dPpYHV5BsMyu4tQ


Generated by Claude Code

…ent no scan root reaches

`SCAN_ROOTS` answers "what is scanned". Nothing answered "is anything in the
agent tree NOT scanned", and a `minFiles` floor cannot: it detects a root that
COLLAPSED, never a document that was never on the surface. objectui#7403 lost 18
fenced blocks to a move that no floor could see; the same shape one step out is a
new `.claude/agents/reviewer.md`, where `.claude/skills` still returns its 4
files, every floor is satisfied, every root resolves, and the new document is
simply unjudged.

Adds `COVERAGE_TREES`: trees in which every `.md`/`.mdx` must be reached by some
declared root. Coverage is a set-membership test over the walk's OWN output --
`scan` collects every path `listDocuments` returned, then asks the same
`listDocuments` for the tree -- so it cannot disagree with the scan about what a
document is. A second glob would be a second answer to one fact.

Two trees. `.claude` is the card's case: the declared row is a proper subtree, so
the tree can grow documents outside it (4/4 covered today). `skills` is the
generalisation, measured: its row is the whole tree, so the test is a tautology
today (16/16) and costs one extra walk of 16 files -- declared anyway because it
stops being a tautology under the one edit nothing else catches, NARROWING or
DELETING the row. A vanished root is loud; a deleted row leaves nothing behind to
be loud about. `content/docs` is deliberately not a coverage tree: same
mechanism, but a completeness claim over published prose that no card has ruled.

An unresolvable coverage tree is loud for the reason a scan root is, and the
coverage figure rides in the verdict line so a green shows its population.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MM7kaS4dPpYHV5BsMyu4tQ

Copy link
Copy Markdown
Contributor Author

Standing down on Live E2E (informational) — red on the base branch too, not this PR's. domain:devx @ objectui execution seat, PM session session_01MM7kaS4dPpYHV5BsMyu4tQ, R45, 2026-09-06T08:41Z. Same signature as main's scheduled run 34017174769 (job 101442890465): the published backend boots without its auth core (objectstack#16186); consumer-side anchor #7990. This diff is one scripts/ gate and its test — no backend pin, no e2e/ path. No fix to port, no re-run spent. Flip and arming wait on the remaining shards (ACCEPT 5558086543 on #7413).


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants