Uh oh!
There was an error while loading. Please reload this page.
feat(tooling): scan agent-facing fenced blocks for machine-produced shell-escape residue - #6216
Conversation
…hell-escape residue objectui#5150 shipped a `git commit -F -` example in AGENTS.md whose heredoc terminator was wrapped in the single-quote-inside-single-quote shell escape. Copied verbatim it does not fail with a message -- it HANGS, on a terminator that never matches, and a reader does not attribute that to the document. objectui#5151 measured the full derived gate union against the replanted bytes: check-control-bytes, check-doc-links, check-changeset-presence and check-changeset-no-major all exited 0. None negligently -- the residue is printable ASCII inside a code block and no scan surface reached it. Adds `scripts/check-shell-escape-residue.mjs`, direction 2 of that card: a narrow literal scan over AGENTS.md, CLAUDE.md, skills/** and content/docs/**. The gate is green at rest (zero occurrences today), so it prints a per-root census rather than a bare OK, fails when the population collapses, and reports a scan root that does not resolve BY NAME -- a mistyped root and a clean root are otherwise indistinguishable. It checks an enumerated literal and says so: executability of fenced shell examples remains UNGUARDED, and `bash -n` per block (direction 1) is the unbuilt option. That boundary is asserted as a fact in the test suite -- broken shell is fed to the gate and a pass is required -- rather than pinned as a sentence. Wiring: root package script, an unfiltered workflow subscribing merge_group, and classification in REQUIRED_CONTEXTS, which is what the merge-queue floor derives from. No governed file is edited: the run over this tree found zero residue anywhere, in governed surface or in docs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
yinlianghui-tw
commented
Aug 25, 2026
PM review — ACCEPT on substance. ⛔ Not armed (7 checks running), and there is a merge-order collision with PR #6212 to settle first.Reviewed by the ⭐ |
| file | both insert | result |
|---|---|---|
scripts/dependabot-merge-gate.mjs | a docstring line after vi-mock-specifiers.yml Inert vi.mock Specifier Check, and an array entry after 'Inert vi.mock Specifier Check', | conflict |
package.json | a check:* script after "check:vi-mock-specifiers" | conflict |
content/docs/guide/ci-cd-pipeline.md | an inventory table row after the vi-mock-specifiers.yml row, plus a new section | conflict |
Whichever merges second will be rejected by the queue and needs main merged in and the three insertions reconciled. This is not a defect in either PR and neither should be changed pre-emptively — it is a scheduling fact, and the queue is the right arbitrator.
Order: this PR first. Not on merit — #6212 is currently red on an unrelated test of its own and is already in a fix round, so it is merging second by circumstance. Its dev has been told to merge main and resolve. ⛔ Do not rebase or force-push this branch to get ahead of it; a merge commit onto main is not what either needs.
REQUIRED_CONTEXTS grows by two once both land, and each of you validated against a tree carrying only your own addition. The queue rebuilds each entry on the updated main, so a genuine interaction reds there rather than on main. That is the queue doing its job, not a gap.
The ablation is the right shape, and the corpus problem is handled honestly
⭐ The mutation line was extracted from da6eda06e^:AGENTS.md rather than retyped — and never routed through a sed/perl expression. That matters more than it looks: this gate's subject is a shell-quote escape run, so retyping it or passing it through a shell-quoting layer is precisely how you end up planting something adjacent to the real defect and proving the gate catches a thing that never happened.
Predicted 8 findings and exit 1; observed exactly 8, each naming its fence and opening line. Restore proven by anchored counts back to 0, git diff HEAD empty, and — the part I like — the post-restore census line compared byte-for-byte against the pre-ablation one with diff, not eyeballed.
The zero-residue result is reported as what it is: the tree is clean, so content/docs supplied no validation corpus and the planted ablation plus the fixture suite are the corpus instead. A gate that is green at rest and says so, prints a per-root census rather than a bare OK, fails when the population collapses, and reports an unresolved scan root by name — because a mistyped root and a clean root are otherwise the same output. That last one is the non-vacuity clause that most gates in this repo have had to learn the hard way.
Governed surface — correctly untouched
The scan found zero residue in AGENTS.md, CLAUDE.md and skills/**, so there was nothing to fix and no governed file is edited by this PR. Had there been residue, the repair would have needed a separate draft PR for a human. Reporting "nothing to report" rather than finding something to touch is the right outcome.
Direction 1 not built, with its extraction-convention dependency recorded in the script header and the CI guide rather than dropped — that is the ruling honoured, and the deferral left findable.
One process note, which is my defect and not yours
an intermediate loop of mine invoked a non-existent
scripts/check-doc-fences.mjsand read node's missing-file exit 1 as a gate failure; the real script ischeck-doc-fence-languages.mjs
⭐ Thank you for recording it and for stating that no conclusion rests on that run. The npm script is check:doc-fences and the file is check-doc-fence-languages.mjs; I have conflated those two in dispatch orders repeatedly tonight, and it is already filed as objectstack#11901. node some/missing/file.mjs exits 1, which is indistinguishable from a gate that ran and failed. A missing-file exit is a broken instrument, not a reading — the same class as an empty grep with no positive control.
⛔ Not arming yet
Type Check, Lint, Doc Snippet Type Check, Live E2E and all four Test shards are in_progress. in_progress is not green. I will arm when every check carries a conclusion — and after #6212's fix round has settled the merge order.
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Fixes#5151
Direction 2, as ruled: a narrow literal scan for known machine-produced shell-escape residue across
AGENTS.md,CLAUDE.md,skills/**,content/docs/**. Direction 1 (bash -nper block) is not built — its dependency on the extraction convention is recorded in the new script's header and in the CI guide so nobody re-proposes it blindly.The defect behind this card
#5150 shipped the
git commit -F -example inAGENTS.md§9 with its heredoc terminator wrapped in the single-quote-inside-single-quote shell escape. Copied verbatim it does not exit with a message — it hangs, on a terminator that never matches, and a reader does not attribute a hung terminal to the document. #5151 measured the full derived gate union against the replanted bytes:check-control-bytes,check-doc-links,check-changeset-presenceandcheck-changeset-no-majorall exited 0. None negligently — the residue is printable ASCII inside a code block and no scan surface in this repository reached it.⭐ The ablation — the only evidence this gate exists
The gate is green at rest (PR #5152 removed the only occurrence that has ever existed), so a green run over today's tree proves only that today's tree is clean. Both directions were predicted before running.
Mutation leg.#5150's exact line was extracted from
da6eda06e^:AGENTS.md— never retyped, never passed through ased/perlexpression — and planted inside a fencedbashblock in one document per scan root. On-disk proof is an anchored occurrence count per file, not an editor's exit code:Two runs per line, because the shipped line carries the escape around the heredoc introducer's terminator and around its repeat. Predicted 8 findings; observed exactly 8, exit 1, each naming file, line, column, the fence language and the line the fence opened on:
Restore leg.
git checkout HEAD --with the explicit paths;git diff HEADempty; anchored counts back to 0; gate exit 0 with a census line byte-identical to the pre-ablation run (diffreported no difference). The planting script carried atrap ... EXIT INT TERMrestore throughout, so a foreground-cap kill mid-mutation could cost a reading but never leave a mutated tree.Both legs were re-run at the committed head
724d1d884.Non-vacuity
The verdict line carries the per-root census, not a bare
OK:FENCE_FLOORis a failure, not a pass.⭐ The header states what the gate does NOT do — and the tests assert it as a fact
Named
shell-escape-residue, notshell-examples: it checks an enumerated literal (one entry, the one observed instance). Executability of fenced shell examples is UNGUARDED and nothing in this repository guards it.That boundary is not pinned as a sentence — this lane spent a round (#6186) on a
toContainasserting a claim exists rather than that it is true. Instead the suite feeds the gate broken shell and requires a pass: an unterminated heredoc, aforwith no list, an unbalanced quote, anifwith nothen; plus the indented-terminator hang the triage note recorded against direction 1; plus the equivalent'\''spelling, which is the documented remedy and is deliberately not matched. Widen this gate into a real syntax check and those cases go red and must be rewritten deliberately.The run over this tree found zero residue anywhere: none in the governed files (
AGENTS.md,CLAUDE.md,skills/**) and none undercontent/docs/**. So there was nothing to report and nothing to fix, and no governed file is touched by this PR — it stays a landable, non-human-merged change.skills/**is scanned but not edited, so no published-skill line budget applies.Wiring, derived from
package.jsonand.github/workflows/rather than from the orderscripts/check-shell-escape-residue.mjsscripts/__tests__/check-shell-escape-residue.test.ts.github/workflows/shell-escape-residue.ymlpull_request+push+merge_group, no path filter, no installpackage.jsoncheck:shell-escape-residuescripts/dependabot-merge-gate.mjsShell Escape Residue ScaninREQUIRED_CONTEXTScontent/docs/guide/ci-cd-pipeline.mdci-cd-pipeline-doc.test.tsrequires both)merge_groupis subscribed and the check is classified inREQUIRED_CONTEXTS, which is what the merge-queue floor now derives from (#6160 / PR #6187). It is deliberately not added toMUST_SUBSCRIBE_MERGE_GROUPinmerge-queue-reporting.test.ts— that hand map is a legacy subset of the derived floor, and the derivation covers the new workflow the moment the context is classified.check-pre-install-import-graph.mjspicked the new script into its derived population automatically and passes: the import graph is node builtins,./invoked-as.mjsand./check-doc-fence-languages.mjs, whose own graph is builtins only.One fence walker, not a second copy:
scanFencesis imported fromcheck-doc-fence-languages.mjsrather than re-implemented (#3261 / #3279).The one deliberate narrowing
Occurrences are judged inside a fenced block and counted everywhere else, with the outside-fence figure in the census. Prose about this defect class has to be able to name the literal — #5151's own body does — and no mechanical rule separates quoting it from shipping it in running text.⚠️ Residue in an inline code span is copy-pasteable too and is not judged; that is a known gap, and the census figure is what keeps it a number rather than a silence.
Verification — all at
724d1d884Tests derived from the files touched, not guessed:
Every exit code was captured by redirect before any pipe.
Declared narrowing:
check:doc-snippetsandcheck:doc-typeswere not run locally — both need an install and a package build. Their population is measured unchanged rather than assumed: the fenced-block count in the onlycontent/docsfile this PR edits is 6 before and 6 after, and the repo-widecontent/docsfence census is 1056 in both the pre-edit and post-edit runs. The new doc section adds no fenced block. CI runs the farm regardless.No changeset:
check-changeset-presencereports 3 changed files, 0 of them published source of a released package.Generated by Claude Code