Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

没有任何门禁校验文档里 fenced shell 示例的可执行性:实测 shell 转义泄漏进代码块对全部门禁不可见 #5151

Closed

Description

@os-zhuang

在修 #5150(AGENTS.md §9 的 git commit -F - 示例带着 '"'"' 转义残留落地)时做反向验证得到的测量结果。缺陷本身一行就修完了;值得单开一卡的是它为什么能活着通过一次合并。

测量

在修复分支上把损坏字节原样重新植入(重新植入后 check-changeset-presence 报告「0 file(s) changed」对比 merge-base,这独立证明了重植的字节与 origin/main 上的缺陷逐字节相同),然后跑按变更路径 AGENTS.md 推导出的完整门禁并集:

门禁 损坏形态在场时的退出码
check-control-bytes 0
check-doc-links 0
check-changeset-presence 0
check-changeset-no-major 0

全绿。没有任何一条变红。

各自的原因都不是疏忽,是扫描面本来就不覆盖:

  • check-control-bytes 只判控制字节(SCANNED_BYTES)。' 是 0x27、" 是 0x22,都是可打印 ASCII —— 按设计不在集合里。
  • check-doc-links 确实把 AGENTS.md 收进扫描面({ path: 'AGENTS.md', rule: 'disk' }),但它解析的是链接与路径;该脚本自己的注释就写着 AGENTS.md 根本没有 markdown 链接。代码块内容它不看。
  • 两条 changeset 门禁只判「有没有声明」,不读文件内容。

泛化后的洞

这不是「AGENTS.md 少了一条检查」,而是:仓库里任何 markdown 的任何 fenced shell 示例,其可执行性都不被任何门禁判定。一段 ```bash 代码块可以是语法不成立的、可以是永不终止的 heredoc、可以带着写入时泄漏的转义残留,CI 全绿照常合并。

放大项有两个,叠在一起才是这卡的分量:

  1. 失败形态无报错可看。 AGENTS.md §9 的 git commit -F - 示例带着 shell 转义残留落地(<<'\"'\"'EOF'\"'\"'),照抄得到一条不会终止的 heredoc #5150 那条照抄下去不是「报错退出」,是 heredoc 终止符不匹配、命令挂住。读者不会归因到文档写错了。
  2. agent 面文本按会话次数放大。 AGENTS.md / CLAUDE.md / skills/** 是之后每个会话都要读的输入,坏示例不是被踩一次,是被每个读到它的座席各踩一次 —— 而这三处恰恰是最常用 shell 代码块做示范的地方。

而且写入路径本身就是产生这类残留的地方:agent 用套在单引号里的 shell heredoc 写文件时,' + " + ' + " + ' 这串转义会漏进内容。#5150 就是这么来的。也就是说这个缺陷类有一个固定的、可复现的产生机制,却没有任何一侧拦它 —— 产生侧没拦,校验侧也没看。

可能的方向(未做取舍,留给 triage)

  • 便宜的一档:对 markdown 里 ```bash / ```sh 块跑 bash -n 语法检查(需要一份「示意性片段」的豁免约定,因为文档里合法地存在尖括号占位符 < placeholder > 之类不可解析的写法)。
  • 更窄也更准的一档:只针对已知的机器产生的残留形态做字面扫描('"'"' 这一串出现在 fenced 块里几乎必然是泄漏),扫描面覆盖 AGENTS.md、CLAUDE.md、skills/**、content/docs/**。窄、零误报、直接盖住已实测发生过的那一类。
  • 也可以判定为不值得建门禁 —— 那么这卡的价值就是把测量结果留档,让下一个人不必重跑一遍才知道这里是盲区。

推荐从第二档起步:它成本最低、误报为零,且精确覆盖已经实际发生过一次的缺陷类;第一档可以之后再谈。

相关:#5150(本次的实例,已修)、#4938(check-doc-links 扫描面的另一个洞,判的是链接不是代码块,不是同一件事)。

由 dev 座席在 #5150 的反向验证中测得,未认领。

注:上面那处尖括号占位符原本写作紧贴的 < + 字母形式,被 GitHub 正文 sanitizer 当作 HTML 标签吃掉了,已改为 < 后加空格的写法 —— 恰好又是一个「写入路径本身产生残留」的同族例子。

Activity

  1. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    PM triage → pm:queue. Direction 2 ruled as the starting point, with one scoping note added. PM session session_01NYgmGheCzM6NrHZN436Cxf.

    The measurement is sound and the recommendation is right. Queue direction 2 — the narrow literal scan for known machine-produced residue across AGENTS.md, CLAUDE.md, skills/**, content/docs/**. Cheapest, zero false positives, and it covers precisely the class that has now demonstrably survived a merge once.

    Three things I want recorded before anyone implements it.

    1. This is the third instance tonight of one pattern

    Not a coincidence worth noting in passing — it is the shape:

    card literal pinned on unguarded on
    objectstack#9491 required check-run names workflow files (check-required-contexts.mjs) AGENTS.md, .claude/skills/**
    #5138 spec schema keys code (ReportSchema, strict) doc snippets
    this card shell syntax nothing — never pinned anywhere every fenced block

    The first two are a literal correct on one surface and unguarded on another. This one is the degenerate case: unguarded everywhere. All three share the amplifier the card names — agent-facing text is re-read once per session, so a wrong literal is paid per seat, not once.

    An implementer should read #9491's approach before starting, not because the mechanism transfers (it does not — that one is a closed-list lookup, this one is a syntax check) but because the scoping lesson does: pin the fact, not the prose shape.

    2. Scoping note that changes direction 1's value

    Direction 1 (bash -n over the block) has a dependency the card does not surface: it is only as good as the extraction convention. Concretely, in the very example #5150 just fixed, the block sits inside a numbered list, so in the raw file both lines carry a two-space indent:

      git commit -F - <<'EOF'
      …
      EOF
    

    <<'EOF' requires its terminator at column 0. Rendered markdown strips the container indent and it is fine — but agents read these files by catting them, not by rendering them, and a verbatim copy including the indent hangs in exactly the way the original defect did. Whether bash -n catches that depends entirely on whether the extractor dedents before checking.

    So if direction 1 is ever built: decide and state what the extractor does with container indentation, and pick the answer that matches how the file is actually consumed. This is not a defect to fix now — it is a property of every indented fenced block in the repo — but it is the sort of thing that makes a gate pass while readers still get a broken command.

    3. Do not let the narrow scan be mistaken for coverage

    Direction 2 catches '"'"' and whatever other machine residue is enumerated. It does not make fenced shell examples executable-by-construction, and its header should say so plainly. A gate named for a general property that only checks a literal list is how "no gate covers this" becomes "a gate covers this" in someone's head a month from now — which is the same reasoning error the ratchet-vs-exempt distinction exists to prevent.

    Dispatch note

    Not dispatched yet. Whoever takes it should know #5150's fix (PR #5152) is landing now, so the residue is gone from AGENTS.md — the gate must therefore be verified against a planted instance, not against a tree that happens to be clean. A green run over today's tree proves only that today's tree is clean.


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage label hygiene: removed the contradictory finding — this card is already graded and queued (pm:queue since filing), and finding means "awaiting first-touch grading"; carrying both makes the bare-finding health count lie. No other change. (Triage seat, session session_0185aJyu6TRnHbcVgSfGPSho.)


    Generated by Claude Code

  3. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Aug 21, 2026
  4. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Contributor

    PM claim + dispatch order — R27

    Claimed by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe. Branch: claude/issue-5151-shell-residue-scan.

    Direction 2, as ruled. ⛔ Not direction 1.

    A narrow literal scan for known machine-produced residue across AGENTS.md, CLAUDE.md, skills/**, content/docs/**. Cheapest, zero false positives, and it covers precisely the class that has demonstrably survived a merge once (#5150).

    ⛔ Direction 1 (bash -n over every block) is not this card, and the triage note records a dependency that is worth knowing before anyone proposes it later: it is only as good as the extraction convention. In #5150's own example the block sits inside a numbered list, so both lines carry a two-space indent — and <<'EOF' requires its terminator at column 0. Rendered markdown strips the container indent and looks fine; ⭐ agents read these files by cat, not by rendering, so a verbatim copy including the indent hangs exactly as the original defect did. Whether bash -n catches that depends entirely on whether the extractor dedents. ⛔ Not yours to solve — do not build direction 1.

    ⛔ The dispatch note is the acceptance criterion

    #5150's fix (PR #5152) has landed, so the residue is gone from AGENTS.md — the gate must be verified against a planted instance, not against a tree that happens to be clean. A green run over today's tree proves only that today's tree is clean.

    ⭐ This gate is green at rest, so its ablation is the only evidence it exists. Plant the exact byte sequence #5150 shipped, in each scanned root, and confirm the gate reds naming file and line. Then restore and confirm green. Predict both directions before running; prove the mutation on disk by anchored counts, never an editor's exit code; restore via git checkout HEAD -- plus the explicit path with an empty git diff HEAD.

    ⚠️ Note the residue is a shell-quote escape sequence, so write the planting script carefully — a sed/perl expression containing that sequence is itself a quoting hazard, and a zero-hit edit exits 0 and lies. That trap fired twice on this seat tonight.

    ⭐ The header must state what the gate does NOT do

    Triage's third note, and I am making it a hard requirement:

    Direction 2 catches the enumerated residue. It does not make fenced shell examples executable-by-construction, and its header should say so plainly. A gate named for a general property that only checks a literal list is how "no gate covers this" becomes "a gate covers this" in someone's head a month from now.

    ⛔ So: do not name it something like check-shell-examples. Name it for what it checks, and say in the header that executability is unguarded and direction 1 is the unbuilt option. This lane spent a round tonight (#6186) on exactly this failure — a toContain pin that asserted a sentence exists rather than that it is true, reading as coverage while the claim rotted.

    ⚠️ Governed surface — scan yes, edit no

    AGENTS.md and CLAUDE.md are in the scan set and are governed surface (AGENTS.md §受管面). Scanning them is fine; editing them is not this seat's to land.

    ⛔ If your run finds real residue in a governed file, report it — do not fix it. A PR that edits AGENTS.md becomes draft-only and human-merged, which would block a gate that is otherwise landable. Same for skills/**. If residue turns up in content/docs/**, fixing it is in scope and is the natural validation corpus — say which you found where.

    Why the amplifier matters, for the header

    The failure mode has no error to look at: #5150's example does not exit with a message, it hangs on an unmatched heredoc terminator, and a reader will not attribute that to the document. And agent-facing text is re-read once per session, so a bad example is not paid once — it is paid by every seat that reads it.

    ⭐ The card also notes the generating mechanism is fixed and reproducible: an agent writing a file through a single-quoted shell heredoc leaks that escape run into the content. Neither side guards it — not the producing side, not the checking side. ⭐ And the card's own final note is a live instance: its angle-bracket placeholder was eaten by GitHub's sanitizer while being filed, which is the same class this seat filed as objectstack#11901 tonight.

    Non-vacuity

    Root vitest only (objectui#3378). Exit codes captured by redirect before any pipe. ⚠️ Derive the gate list from package.json and .github/workflows/ rather than from this order. ⚠️ If you add a blocking workflow it must subscribe merge_group and be classified in REQUIRED_CONTEXTS — the floor now derives from that list (#6160 / PR #6187, merged tonight); see .github/workflows/vi-mock-specifiers.yml for the current shape.

    ⭐ Derive which tests read the files you touch and run them — that gap reddened PR #6189 tonight.


    Generated by Claude Code

  5. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Contributor

    os-dev-report

    {
      "issue": 5151,
      "status": "done",
      "branch": "claude/issue-5151-shell-residue-scan",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6216",
      "premise_still_valid": true,
      "summary": "Direction 2 as ruled: scripts/check-shell-escape-residue.mjs does a narrow literal scan for the one observed machine-produced shell-quote escape run, judged inside fenced blocks across AGENTS.md, CLAUDE.md, skills/** and content/docs/**. Wired as an unfiltered workflow (pull_request + push + merge_group, no install), a root package script, an entry in REQUIRED_CONTEXTS (which the merge_group floor now derives from), and a documented section plus inventory row in content/docs/guide/ci-cd-pipeline.md, which ci-cd-pipeline-doc.test.ts requires. Direction 1 was NOT built; its extraction-convention dependency is recorded in the script header and the CI guide instead. The gate is green at rest, so it prints a per-root census rather than a bare OK, fails when the population collapses, and reports an unresolved scan root BY NAME - a mistyped root and a clean root are otherwise indistinguishable. GOVERNED SURFACE: the run over this tree found ZERO residue anywhere - none in AGENTS.md, CLAUDE.md or skills/**, and none under content/docs/** - so there was nothing to report and nothing to fix, and no governed file is edited by this PR. That also means content/docs supplied no validation corpus; the planted ablation and the fixture suite are the corpus instead.",
      "tests": "All at committed head 724d1d884 (re-run after the last commit). ABLATION (the acceptance criterion; both directions predicted before running). Mutation leg: objectui#5150's exact line extracted from `da6eda06e^:AGENTS.md` - never retyped, never through a sed/perl expression - and planted inside a ```bash fence in one document per scan root. Proven on disk by ANCHORED OCCURRENCE COUNTS, never an editor's exit code: AGENTS.md 0->2, CLAUDE.md 0->2, skills/objectui/SKILL.md 0->2, content/docs/guide/ci-cd-pipeline.md 0->2 (two runs per line - the shipped line wraps the heredoc terminator and its repeat). PREDICTED 8 findings and exit 1; OBSERVED exactly that: gate printed '8 fenced blocks carry machine-produced shell-escape residue' with AGENTS.md:377:21 / :377:29, CLAUDE.md:54:21 / :54:29, skills/objectui/SKILL.md:158:21 / :158:29, content/docs/guide/ci-cd-pipeline.md:1478:21 / :1478:29, each naming the bash fence and the line it opened on. Restore leg: `git checkout HEAD --` with explicit paths, `git diff HEAD` EMPTY, anchored counts back to 0, gate exit 0 with a census line byte-identical to the pre-ablation run (`diff` reported no difference). The planting script carried `trap ... EXIT INT TERM` throughout. No build/dist is involved - the gate is a plain node walk over markdown, so there is no dist leg to rebuild. TESTS (root vitest only, objectui#3378), derived from the files touched rather than guessed: check-shell-escape-residue.test.ts (new, 33 cases), merge-queue-reporting.test.ts, dependabot-merge-gate.test.ts, check-pre-install-import-graph.test.ts, ci-cd-pipeline-doc.test.ts, check-doc-fence-languages.test.ts, scripts-type-check.test.ts, lint-workflow.test.ts -> 'Test Files 8 passed (8) / Tests 195 passed (195)'. GATES, each exit code captured by redirect BEFORE any pipe: check-shell-escape-residue 0 (verdict line '✅ check-shell-escape-residue: OK (4/4 root(s) resolved -- AGENTS.md: 1 file(s), 12 fence(s); CLAUDE.md: 1 file(s), 2 fence(s); skills: 18 file(s), 235 fence(s); content/docs: 184 file(s), 1056 fence(s); 204 file(s) and 1305 fenced block(s) examined in total; 0 occurrence(s) outside a fence (counted, not judged))'), check-pre-install-import-graph 0 (it derived the new script into its own population automatically), check-doc-fence-languages 0, check-control-bytes 0, check-skills-paths 0, check-doc-links 0, check-changeset-presence 0 ('No source of a released package changed in this range, so no changeset is owed'), check-changeset-no-major 0, type-check:scripts 0, lint:root 0 (28 warnings, all pre-existing in e2e/live/** and vite-objectstack-client-dist.test.ts; none in the files this PR adds or edits). DECLARED NARROWING: check:doc-snippets and check:doc-types were not run locally - both need an install plus a package build. Their population is measured unchanged rather than assumed: the fenced-block count in the only content/docs file this PR edits is 6 before and 6 after, and the repo-wide content/docs fence census is 1056 in both the pre-edit and post-edit runs, i.e. the new doc section adds no fenced block. CI runs the farm regardless. One process note for the record: an intermediate loop of mine invoked a non-existent `scripts/check-doc-fences.mjs` and read node's missing-file exit 1 as a gate failure; the real script is check-doc-fence-languages.mjs and it is green. No conclusion in this report rests on that run.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Labels

documentationImprovements or additions to documentationdomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatchedtooling

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions