Skip to content

[finding] check-shell-escape-residue's new .claude/skills root does not reach the rest of the agent tree — a future .claude/agents/*.md repeats #7403 exactly #7413

Description

@os-litant

Filed out-of-scope from #7403 (the widening itself). Unassigned. Zero occurrences today — this is an observation about the shape of the row, not a live defect.

What

#7403 widened SCAN_ROOTS in scripts/check-shell-escape-residue.mjs with:

{ spec: '.claude/skills', kind: 'dir', minFiles: 3 },

The seat's in-lane ruling on that card was stated about the tree — ".claude/** belongs on this gate — the residue class it catches is machine-produced shell escaping, and contributor guides are agent-written too" — while the row that landed is the skills subtree of it.

Today the difference is exactly zero files. Measured on e7d68db:

$ find .claude -name "*.md" -o -name "*.mdx"
.claude/skills/objectui-contributor/SKILL.md
.claude/skills/objectui-contributor/guides/console-development.md
.claude/skills/objectui-contributor/rules/no-touch-zones.md
.claude/skills/verify/SKILL.md

4 of 4 under .claude/skills/, so the two specs read the same tree and the narrower one loses nothing. The row is annotated with that reading and with this gap.

Why it is worth a card anyway

The day someone adds .claude/agents/reviewer.md or .claude/commands/ship.md with a fenced shell example, it is agent-written and agent-read prose carrying fences that no root reaches — and nothing goes red, for the same reason #7251's move was invisible: a minFiles floor is a collapse detector, not a coverage detector, and .claude/skills will still be returning its 4 files. That is #7403's own class one step out, and #7403's own lesson is that this shape is only ever noticed by someone going and looking.

Not a duplicate of

Suggested shape, not prescribed

Two directions, and the choice is a decision rather than a mechanical fix:

  1. Widen the row spec to .claude. Reads identically today, closes the next move by construction. Cost: it also pulls in any future .claude/** markdown that is not agent prose, and the gate's own doc would need to say what that root claims.
  2. Leave the row and add the check at the other end — a floor-style assertion that every .md/.mdx under .claude/ is under a declared root, so a new agent-tree document is a red gate rather than a silent gap. That is the general form of the miss and would cover the skills/ side too.

Either way the arithmetic to re-measure is in the gate's docblock and in PR for #7403.

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Sep 2, 2026
  2. self-assigned this
    on Sep 6, 2026
  3. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    Claim: PM loop round R45
    Session: session_01MM7kaS4dPpYHV5BsMyu4tQ
    Branch: claude/issue-7413-shell-escape-residue-claude-root
    Worktree: objectui-issue-7413
    Domain: domain:devx
    File surface: scripts/check-shell-escape-residue.mjs (the .claude coverage assertion — direction 2 of the card: every .md / .mdx under .claude/ must be under a declared SCAN_ROOTS row, red otherwise; ⛔ not a silent widening of the row spec without that assertion) and its test file under scripts/__tests__/ (the pin: a planted .claude/agents/x.md outside every root reds; the four files on main today are covered); ⛔ nothing under .claude/** itself beyond a temporary planted fixture inside the test (governed surface) (stop on breach; explain in the report)
    Container & model: S, not mechanical (the card names two directions and the seat picks one — the dev measures whether direction 2 also covers the skills/ side as the card suggests), mode:subagent, model: opus — no path-derived tier mandate on scripts/** (floor sonnet · default opus · ceiling fable); default judgment tier taken
    Clause-②: no
    Serial constraints cleared: no triage comment on this card (it carries pm:queue from the sweep); the seat's route is the card's direction 2 — the general floor — because direction 1 (widen the row to .claude) reads identically today and still leaves the skills/ side unmeasured; premise on origin/main 15789d5 at 08:17Z — the row { spec: '.claude/skills', kind: 'dir', minFiles: 3 } at :250, exactly four markdown files under .claude/, all under .claude/skills/; no open PR touches the gate (file lists read 06:27Z; #7685's 67 paths over git: 0); this seat's in-flight batch (#5174 b18 in one packages/*/README.md + the doc-snippet gate; #7362 in content/docs/guide/) is disjoint. pm:queue → pm:dispatched + assignee written in one stroke and read back.


    Generated by Claude Code

  4. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    os-dev-report

    {
      "issue": 7413,
      "status": "done",
      "branch": "claude/issue-7413-shell-escape-residue-claude-root",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8010",
      "premise_still_valid": true,
      "summary": "Direction 2, implemented in the gate itself rather than only in its test, because that is where the sibling `minFiles` / `unresolved` judgements live and the card asks for a RED GATE. `scripts/check-shell-escape-residue.mjs` gains `COVERAGE_TREES` — trees in which every .md/.mdx must be reached by some SCAN_ROOTS row — plus an `uncovered` finding list, a coverage figure in the verdict line, and loudness for a coverage tree that does not resolve (role-tagged so the remedy names the right declaration list). Coverage is set membership in the walk's OWN output: `scan` accumulates every path `listDocuments` returned for every resolved root, then asks the same `listDocuments` for the tree — one walk, one DOC_EXTENSIONS, so it cannot disagree with the scan about what a document is. The `skills/`-side generalisation was measured and INCLUDED: its row is the whole tree so the assertion is a tautology today (16/16) at the cost of one extra walk of 16 files, but it stops being one under the single edit nothing else here catches — narrowing or DELETING a SCAN_ROOTS row, which unlike a vanished root leaves no signal behind. `content/docs` was deliberately excluded (published prose, a completeness claim no card has ruled) and filed as #8011 rather than decided unilaterally. No .claude/** file was modified; the planted fixture existed only inside a trapped ablation run and the tree is clean.",
      "tests": "REVERSE VERIFICATION (on disk, real gate, inside a trap): planted .claude/agents/reviewer.md with a fenced bash block — on-disk proof `BEFORE: .claude md count = 4, reviewer.md present = no` / `AFTER plant: .claude md count = 5, reviewer.md present = yes`; gate EXIT=1 printing `check-shell-escape-residue: 1 agent-tree document is on no scan root` and `- .claude/agents/reviewer.md (under the coverage tree .claude)`; after removal `RESTORED: .claude md count = 4, agents dir present = no`, gate EXIT=0, `git status --porcelain` empty. ABLATION of the implementation (committed first, then mutated): detection line `const missing = documents.filter((rel) => !scanned.has(rel));` replaced by `[]`, landing proven on disk by anchored grep counts (original 1 -> 0, marker 0 -> 1) and by a blob-hash mismatch against the HEAD blob 57be819; MUTATED vitest EXIT=1, `Tests 4 failed | 39 passed (43)`, the four being the card's scenario, the row-deletion case, the same-walk case, and the pre-existing out-of-scope case; restored via `git checkout HEAD -- path`, `hash matches HEAD? YES`, `git diff HEAD` empty. Predicted direction was RED and RED is what was observed. Re-run at the final head 150ff53 after a docblock amend, same result. NOT MEASURED: nothing — every command below ran to a real verdict.",
      "gates": "All at final head 150ff53, exit codes captured by redirect-then-capture, verdict lines quoted from each gate's own output. `node scripts/check-shell-escape-residue.mjs` exit 0 — `check-shell-escape-residue: OK (5/5 root(s) resolved -- AGENTS.md: 1 file(s), 15 fence(s); CLAUDE.md: 1 file(s), 0 fence(s); skills: 16 file(s), 210 fence(s); .claude/skills: 4 file(s), 20 fence(s); content/docs: 184 file(s), 1064 fence(s); 206 file(s) and 1309 fenced block(s) examined in total; 0 occurrence(s) outside a fence (counted, not judged); coverage -- .claude: 4/4; skills: 16/16 document(s) under a declared root)`. `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__/` (whole suite) 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)`; the 32 warnings are the pre-existing baseline and none is in either changed file (an earlier run read 33 because this change introduced one unused type; fixed by using it, re-measured at 32). `pnpm check:control-bytes` exit 0 — `check-control-bytes: OK (scanned 6440 tracked text file(s); skipped 85 binary)`; plus the manual self-scan the clause requires, `grep -naP` over both changed files: clean. `node scripts/check-changeset-presence.mjs` exit 0 — `2 file(s) changed, 0 of them published source of a package the release covers ... No source or published contract of a released package changed in this range, so no changeset is owed`; none owed, so no changeset and no label (objectui's skip-changeset label is inert and is never applied here). `node scripts/check-governed-queue-guard.mjs --test` on the two changed paths exit 0 — `NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched`. CI not awaited: reporting at draft-PR time per contract. `Live E2E (informational)` is red on every branch today for the upstream reason (#7990 / objectstack#16186).",
      "line_budget": "Not applicable — no skills/** file is touched. The diff is scripts-only: +351 / -25 over 2 files (gate +153/-?, its test +223/-?; 351 insertions, 25 deletions in total). No published skill package, no SKILL.md, so no net-increase ceiling applies and none was spent.",
      "deviations": "(1) A3 FALSIFIED: the PM's assumption that UNSCANNED-style exclusions (node_modules etc.) exist in the gate's walk is wrong — `listDocuments` has no exclusion list of any kind and recurses into every directory under a root, filtering only by DOC_EXTENSIONS. No exclusion tokens exist anywhere in the file. This made the requirement EASIER, not harder: rather than mirroring an exclusion list, coverage reuses the same `listDocuments` and tests membership in the set the scan actually walked, so agreement is by construction rather than by duplication. A1 and A2 both confirmed as stated (row at :250 verbatim, 4 of 4 markdown files under .claude all beneath .claude/skills; the existing test drives the gate through its exported `scan`/`listDocuments`/`findResidue` and was extended, not paralleled). (2) The assertion was placed in the GATE, not only in its test, which the dispatch left to the dev after reading how the minFiles floors are checked: they are computed in `scan()` and merely pinned by the test, and the card asks for a RED GATE. The test carries the ablation, six new cases, and the repo-population pin. (3) Base is e1545cf, newer than the 15789d5 the claim measured; the premise re-verified identically on it. (4) objectui has no scripts/pm/os-verify-lock.sh — the shared verify-lock entry point is an objectstack mechanism and does not exist in this repo; every command here is light (scripts-only vitest, tsc, root-scoped eslint) and was run in the foreground with no lock. (5) No local narrowing to declare: `pnpm lint:root` is the repo's own root-scoped script and was run whole, and the full scripts/__tests__ suite was run rather than only the touched file. (6) One commit was amended twice — once to strip a `Fixes #7413` trailer out of the commit message (card relations belong in the PR body only, since squash concatenates messages), once to move the new docblock section to the end so it stopped splitting a pre-existing heading from its body; the full gate battery and the ablation were re-run at the resulting final head.",
      "files_changed": [
        "scripts/check-shell-escape-residue.mjs",
        "scripts/__tests__/check-shell-escape-residue.test.ts"
      ],
      "open_questions": [],
      "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 and must not be given one — it deliberately carries no path filter at all. Its header, quoted: `Hence: no paths and no paths-ignore here, deliberately. scripts/__tests__/check-shell-escape-residue.test.ts fails if either is ever added, and fails too if a second workflow starts running the same script.` That pin (`runs it in NO path-filtered workflow`) is among the 43 green cases. So the widened judgement reaches every pull request shape with no workflow change, and the check stays requirable; `REQUIRED_CONTEXTS` still lists `Shell Escape Residue Scan` and the merge_group subscription is unchanged.",
      "out_of_scope_findings": [
        "filed as #8011 (label `finding`): check-shell-escape-residue's `content/docs` row has no coverage tree, so narrowing or deleting that row is still silent — 184 files and 1064 fences, 81% of the gate's fence population, sit behind it; measured that FENCE_FLOOR catches this one case today only by accident (1309-1064 = 245, under the 400 floor by 155) and stops doing so as soon as the other trees grow. Three options laid out, including pinning the SCAN_ROOTS spec list to catch deletion of any row generally.",
        "filed as #8012 (no label — concrete defect): scripts/__tests__/check-shell-escape-residue.test.ts's docblock has an empty `## Fixture discipline` heading whose body sits two sections lower under the wrong heading, introduced when #7403's section was inserted between the heading and its paragraph; PR #8010 hit the same fork and moved its new section to the end rather than fix it in an unrelated PR."
      ]
    }

    Generated by Claude Code

  5. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    ACCEPT — PR #8010, head 150ff538e; flip and arming on CI convergence

    domain:devx @ objectui execution seat, PM session session_01MM7kaS4dPpYHV5BsMyu4tQ, R45, 2026-09-06T08:39Z. Report reviewed against GitHub and the tree, ⛔ not against its narrative.

    item reading (mine)
    PR shape draft ✅ · base main ✅ · first line Fixes #7413 ✅ · 2 files ✅ (the gate and its test; nothing under .claude/**)
    The fix, read (+153/−25 in the gate, non-comment lines) COVERAGE_TREES = [.claude, skills]; scan() accumulates every path listDocuments returned for every resolved root into a scanned set, then walks each coverage tree with the SAME listDocuments and reports missing = documents.filter(rel => !scanned.has(rel)) as uncovered; the verdict line gains a coverage -- .claude: 4/4; skills: 16/16 figure; unresolved entries are role-tagged (scan root / coverage tree) so the remedy names the right list; the red message says explicitly that a minFiles floor detects a collapse, not a never-covered document, and offers two deliberate remedies with no per-file opt-out. Direction 2 in the gate, where the sibling floors live — a RED GATE, as the card asked
    A3 falsified, to the fix's benefit the walk has no exclusion list at all; coverage is membership in the walk's own output, so the two cannot disagree by construction
    The pin, read (+223, nine new cases) control green on the healthy fixture; RED on the card's own scenario (.claude/agents/reviewer.md); GREEN again when the same document moves under a declared root; RED when a SCAN_ROOTS row is DELETED (the shape with no other signal — the skills/-side generalisation earns its place here); same-walk agreement; an unresolved coverage tree is reported, not silently empty; the repo population asserted (4/4 under .claude)
    Reverse verification on-disk plant of .claude/agents/reviewer.md inside a trap → gate exit 1 naming the path; removal → exit 0, tree clean; ablation (detection line replaced by []) → 4 tests red, restored by blob hash
    Gates gate exit 0 with the new coverage figure · its test 43/43 · whole scripts/__tests__/ 107 files / 3253 tests · type-check:scripts · lint:root 0 errors (an unused type the change introduced was caught and used) · control-bytes · changeset-presence none owed · governed guard NOT GOVERNED ×2
    Readers the workflow shell-escape-residue.yml deliberately carries no path filter (pinned), so the widened judgement reaches every PR shape with no workflow change; REQUIRED_CONTEXTS untouched
    Out of scope #8011 filed (content/docs has no coverage tree — deleting that row is still silent; FENCE_FLOOR catches it today only by 155 fences of margin) · #8012 filed (an orphaned ## Fixture discipline heading in the test's docblock)
    CI on 150ff538e at 08:38Z 17 success, 3 skipped, 9 in progress

    On all-green: standing-down note if the informational lane reports red, flip ready, wait for the post-flip guard, arm, confirm added_to_merge_queue. On MERGED: content probe on re-fetched origin/main (COVERAGE_TREES in the gate; the REDS on this card's own scenario case in its test; control — SCAN_ROOTS still present), card closes via Fixes, strip pm:dispatched, clear assignee.


    Generated by Claude Code

  6. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    Hand-over note — session_01MM7kaS4dPpYHV5BsMyu4tQ (devx@objectui, R45) is closing at 2026-09-06T08:53Z on the maintainer's instruction. Caretaker: the next domain:devx @ objectui seat (closing brief on seat post #5748). The card stays pm:dispatched.

    PR #8010 (claude/issue-7413-shell-escape-residue-claude-root, head 150ff538e, draft) — ACCEPT 5558086543, standing-down note for the informational Live E2E row 5558090981. Left for the caretaker, as the ACCEPT says: on every non-skipped check green except Live E2E → flip ready, wait for the post-flip guard, enable auto-merge (squash), confirm added_to_merge_queue. On MERGED: probe re-fetched origin/main — COVERAGE_TREES present in scripts/check-shell-escape-residue.mjs, the "REDS on this card's own scenario" case present in its test, control: SCAN_ROOTS still present in scripts/__tests__/doc-version-claims.test.ts; the card closes via Fixes → strip pm:dispatched, clear assignee, LANDED note.


    Generated by Claude Code

  7. removed their assignment
    on Sep 6, 2026
  8. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    LANDED — PR #8010 merged 2026-09-06T09:37:03Z (queued 09:22:35Z). Verified on origin/main eeda78a by CONTENT, ⛔ not by the merge event.

    domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-06T09:55Z — caretaker landing stroke per the R45 hand-over note 5558150033 and ACCEPT 5558086543.

    reading expected got
    COVERAGE_TREES in scripts/check-shell-escape-residue.mjs ≥ 1 6
    the "REDS on this card's own scenario" case in scripts/__tests__/check-shell-escape-residue.test.ts 1 1
    control — SCAN_ROOTS still in the gate ≥ 1 13 ✅ probe fires

    Card closed via Fixes at 09:37Z; pm:dispatched stripped and assignee cleared in this stroke (labels now enhancement / finding / priority:p3 / domain:devx).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoenhancementNew feature or requestfindingpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions