Skip to content

fix: close the extract-ssot sweep's open remainder, including a fail-closed gate that failed open - #3468

Merged
claude[bot] merged 6 commits into
mainfrom
claude/docs-hygiene-ssot-extraction-jft8sf
Aug 28, 2026
Merged

fix: close the extract-ssot sweep's open remainder, including a fail-closed gate that failed open#3468
claude[bot] merged 6 commits into
mainfrom
claude/docs-hygiene-ssot-extraction-jft8sf

Conversation

@claude

@claude claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

No linked issue

Summary

#3460 merged a whole-repo extract-ssot sweep and recorded, in its own resumable spec, three things it had deliberately left open. This closes them. Four independent agents then audited the merged commit — two remediating, two auditing with the author's account withheld — and one of them found that the sweep's own convention had a live consequence nobody had swept for.

The headline is not a documentation fix. plugins/knowledge/skills/video-digest/SKILL.md:115 reads "STOP if the pre-computed context above shows MISSING for yt-dlp, ffmpeg, or ImageMagick. Cloud agents without the media toolchain fail closed." Its probes were yt-dlp --version 2>/dev/null | head -1 || echo "MISSING …". || binds to the pipeline and head exits 0 regardless, so an absent yt-dlp rendered an empty string, never MISSING. The gate could not fire. A skill documenting itself as failing closed failed open.

Fix

Failure-token fallbacks that could never render, six plugins. video-digest (3 probes, the gated one), course-digest (2), ai-slop:audit, provenance:audit (2), firecrawl:update, claude-ops:observability. Each now uses the guard-first shape firecrawl:firecrawl already shipped correctly, so the fleet gains no new idiom. Proven by execution on both branches: tool absent renders the failure token, tool present renders the real value. No $ expansion is introduced — the composed precompute block must stay verifiable to the worktree-isolation guard, which rules out the capture-first variant.

A grant #3460 left behind. It rewrote the two prototype skills' injections to begin with { while their line-6 grant stayed the prefix rule Bash(git status:*), whose leading token no longer matches. Narrowing, never widening — but a probe that silently stops running is the same class of silent failure as a fallback that cannot render. Upstream brace-group decomposition could not be established from permission-rule-hygiene/README.md, so an exact grant was added alongside rather than assumed.

The five filtered probes #3460 deferred. Their none fallback conflated "the filter matched nothing" with "git did not run". Fixed at audit-noise, audit-progressive-disclosure, compress, dissolve-comments, audit-comment-residue, each keeping its own filter, cap and label noun so the two meanings stay distinguishable.

The ADR 0018 encapsulation floor, closed. The spec recorded two sites and declared the roster a floor. Re-derived across all of docs/** with five independent search shapes: 16 citations at 12 sites in 8 files. Among them V-review-13 and V-review-14, standing open on the predecessor's 34-item roster since 2026-08-26 (34 → 32).

The sharpest one: docs/PLUGIN-PHILOSOPHY.md:616 — the Convention registry row #3460 itself added — was written in the exact bare unresolvable form ADR 0018 names as its real defect class. The sweep introduced a fresh instance of the class it was closing, in the owner doc, in the same commit. Now /playbooks:skill-authoring.

23 citations were kept, on a stated test: fix when the citation is the doc's address for an obligation, so the binding fails where it binds; keep when it is evidence about this tree at a moment in time. The kept set is recorded as a judgment set so it can be overturned.

Five more false claims in the merged record, found by a fourth review round and each re-derived before fixing. Two are structural: "listed under Applied above" pointed at a section naming none of the four clusters, and the paragraph on the two prompt files attributed to them a statement neither makes — the identical wrong-subject quote defect round three fixed, recurring one paragraph away. Three smaller: a pointer recorded at 14 sites where 15 cite it and only 11 share a sentence form; a paraphrase presented as a quotation; and a cluster recorded as four prose surfaces when a fifth sat in a script header a *.md-only grep could not see.

Verification

scripts/check-changed-skills.sh origin/main        13 skills checked, 0 failed
node scripts/validate-plugin-contracts.mjs         52 setup skills, 3022 files
scripts/check-changelog-parity.sh --check          clean
scripts/check-changelog-parity.sh --check-order    86 changelogs, no duplicates
scripts/check-changelog-parity.sh --check-bump     every bumped plugin has its entry
scripts/check-skill-portability.sh origin/main     clean
scripts/affected-tests.sh --run                    exit 3 (documented NOT RUN contract), 0 failed
markdownlint-cli2 <changed>                        0 issues
typos <changed>                                    clean

Execution proof for the gate, run outside any repo with the tool absent and then present:

tool absent   -> MISSING — install yt-dlp (see Prerequisites)     # the gate can now fire
tool present  -> <version>                                        # the real value, not the token

What the security audit tried and could not break. A repo path containing spaces, quotes, $(touch PWNED) and a backtick; files named ; rm -rf victim, `touch PWNED3`, one with an embedded newline; a victim/ directory to destroy; .git/HEAD deleted; git absent from PATH; 188 KB of porcelain through head -5 for the SIGPIPE path. Nothing executed, victim intact, no artifact, no spurious fallback, no stderr leak. It also proved emit-findings.sh's diff is comment-only by md5-comparing the bodies from set -euo pipefail onward.

Honest limits. Both CI Claude review lanes returned HTTP 429 at every head of #3460 and never ran; this session's token gets 403 on rerun-failed-jobs. Subagents in this environment have no Agent tool, so the nested verifiers this work was meant to use could not be spawned — two agents improvised (one hung a headless process, one spawned a session it could not read back) and returned findings marked UNVERIFIED. I re-derived every load-bearing finding myself before acting on it, which is what caught them. One remote verifier reported "3 keeps refuted; 4 cannot verify" on the encapsulation roster and its reasoning is unreadable from here; the kept set is recorded as a judgment set precisely so those can be overturned by a later pass.

Related

Refs #3460, refs #3362, refs #3380. Closes the open remainder recorded in docs/specs/extract-ssot-sweep-2026-08-28.md. Applies ADR 0018. Advances the predecessor's L4 roster in docs/specs/docs-hygiene-sweep-unapplied-remediations.md from 34 to 32.


Generated by Claude Code

claude added 6 commits August 28, 2026 08:38
ADR 0018 makes the plugin the encapsulation boundary for citation: a doc names
a public `/plugin:skill` invocation, never a filesystem path into another
plugin's private tree, because an installed plugin may not be on disk at that
path. The extract-ssot sweep record declared its encapsulation roster a floor,
not a total: it carried two `docs/**` sites, a later pass found three more of
the identical shape on no roster, and none was fixed.

This re-derives the shape across all of `docs/**` rather than trusting the
roster, and closes 16 citations at 12 sites in 8 files. The five already named
plus: the dynamic-context row `docs/PLUGIN-PHILOSOPHY.md`'s Convention registry
gained in the same commit that recorded the floor, written in the bare
plugin-relative form ADR 0018 names as its real defect class; two conformance
rows in `config-cascade`; one in `pre-pr-ordering`; three in
`native-references`, two of which are `V-review-13` and `V-review-14` standing
open on the predecessor's 34-item L4 roster since 2026-08-26; and one adopter-
row detail in `detector-findings` reaching into a private `scripts/lib/`.

The convention READMEs are the sharp case. Nineteen plugin surfaces fetch
`topic-docs` over raw.githubusercontent.com at run time, ten fetch
`detector-findings`, five `config-cascade`, three `permission-rule-hygiene`. For
that reader the repo is not on disk at all, so a `../../../plugins/...` link was
dead exactly where the convention binds. `topic-docs` is the sharpest: the
paragraph arguing that a `SKILL.md` "cannot defer at runtime to a document the
consuming repo does not have" made its case with two links that were precisely
that.

Eighteen further in-shape citations are kept, with reasons, because each is
evidence about this checkout rather than an address for an obligation: worked
examples whose content the citing doc already states, `scripts/` entry-surface
pointers the public-surface contract's own carve-out permits, generated regions
and the store behind them, and dated changelog entries quoting a citation as it
stood. Generated views and the dated records under `docs/specs/`, `docs/adr/`
and `docs/topics/` are out for the same reason.

Changelog entries follow the `loop-lane` 9.0.1 and `detector-findings` 2.7.1
precedents and each convention's own bump rule: `topic-docs` 2.5.3 and
`detector-findings` 2.8.1 as patches, `permission-rule-hygiene` 1.3.1 and
`loop-lane` 9.0.2 as corrective patches, `config-cascade` as an unversioned
Implementers-table entry per its own no-contract-change precedent, and
`native-references` gains the `CHANGELOG.md` its Versioning section promised.
`pre-pr-ordering` and `docs/PLUGIN-PHILOSOPHY.md` declare no version and get no
entry.

`permission-rule-hygiene`'s changelog preamble also stops naming
`permission-hygiene`, a skill renamed to `audit-permission-grants` in #371 and
not carried along since.

The sweep record's recall-limits bullet now states what closed and what was
kept, so the floor reads as resolved rather than open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo
… the probe

The extract-ssot sweep record left four filtered-probe injections open on purpose and
named a fifth under its recall limits. All five collapsed two meanings into one
rendering: `probe | filter | head -N || echo "none"` binds the `||` to the pipeline,
whose exit status is `head`'s, and `head` exits 0 on empty input, so the fallback never
ran and a failed `git status` rendered empty. `compress` inverted it: its fallback sat
inside the brace group behind the filter, where `grep` exits 1 both when git failed and
when nothing matched, so `none` printed for both.

Each line now heads an `&&` list with a status-only run of its own probe:

  git status --porcelain >/dev/null 2>&1 && git status --porcelain 2>/dev/null \
    | <filter> | head -N || echo "(git status unavailable)"

so the `||` fires on probe failure alone. Each site keeps its own filter, cap and label
noun; each label gained `empty = none`. The probe runs twice because the capture-first
alternative in `audit-derivability` needs a `$` expansion, and a `$` expansion other than
a bare `$HOME` in a pre-compute block makes a skill fail to load from a worktree-isolated
agent (`session-flow` 0.17.16). The added text introduces no `$`.

Proven by execution per site, in every state: outside a repository each prints
`(git status unavailable)`; with git absent from PATH and with a `.git` that is not a
repository, the same; inside a repository whose dirty files do not match the filter each
prints nothing; with 15 matching files and a cap of 10 exactly 10 print and the SIGPIPE
that reaches git does not leak into the `||`.

No `allowed-tools` grant changed: three of the five carry one, none of the three grants
`git status`, and the edit introduces no new binary.

Two `detect.test.sh` parity checks extract these lines out of `SKILL.md` by `sed` and run
them; their patterns were anchored on the old labels and now anchor on the label stem.
The `audit-comment-residue` porcelain extractor's capture reaches the first `|`, so it
now takes the whole `&&` list and evaluates the reachability guard as written.

docs/specs/extract-ssot-sweep-2026-08-28.md moves the cluster from open to applied,
records that the candidate shape it proposed was not taken and why, and records one
site left open: `rename-references`'s rename-pair probe has the same binding defect on
a `git diff` probe, where a filter matching nothing is the ordinary case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo
…ecord

An independent claims audit re-derived every count and citation in the merged
record and found five more. Each was verified here against the repository before
being fixed; the audit's own verifier could not run (a subagent has no Agent
tool), so its verdicts arrived marked UNVERIFIED and I re-derived them myself.

Two are structural defects in the resumable record, which is the file's whole
job:

- "Four of the factual defects ... are listed under Applied above" pointed at a
  section that names none of the four. That section covers the owner edits, the
  inline floor and the call-site normalizations; a resumer following the pointer
  found nothing. It now says where the detail actually is.
- The paragraph on the two prompts claimed both "say in their own text that the
  quotation sits outside the byte-audited block". Neither does. The only
  sentence in either naming that block is "Two further reader-contract rules
  apply alongside the floor (outside the byte-audited block)", whose subject is
  those two rules. The conclusion held for a different reason -- section 6 binds
  three lane bodies and a prompt is not a lane -- but the support was a quote
  attached to the wrong subject, which is the identical defect this record was
  corrected for in round three, recurring one paragraph away.

Three smaller ones. The discipline shared-method pointer was recorded at 14
sites; 15 skill bodies cite it and only 11 share one sentence form, so 14
matched neither population. The predecessor's cell was quoted as "41 instances,
34 edited" where it reads "41, 34 edited" -- faithful in meaning, not a
quotation. And the check-skill.sh:414 cluster was recorded as four prose
surfaces when a fifth carrier sat in a script header that a *.md-only grep could
not see; all five are closed, only four were recorded as such.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo
A second, independently-tooled derivation of the same shape found a file the
first pass missed. Matching `<skill>/<subdir>/` and `<skill>/<file>` against the
real skill-name roster, rather than only `plugins/*/skills/`, reaches
`docs/MIGRATION-PLAYBOOK.md`'s dated `dometrain` security-review record, which
cites `sync/scripts/update.sh` twice, `sync/SKILL.md` twice, and
`grounding/SKILL.md` once.

All five are keeps under the same test the rest of the roster uses: the record
asserts what a named file contains at a named version, and re-triggers on the
next version bump, so the path is the evidence rather than an address for an
obligation. Two of the five are `scripts/` entry-surface cites the
public-surface contract's carve-out permits outright.

Nothing in the fix set moves. The kept count goes from 18 to 23, which is what
the recall-limits bullet now says. The miss is worth recording rather than
quietly correcting: the first pass keyed on the `plugins/` prefix, and a
plugin-relative citation that never writes it was outside every one of its
search shapes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo
…ized last release

Both 0.21.23 and 0.14.12 opened by saying the prior release "normalized this plugin's
status probes", plural in one and unattributed in the other. Each plugin carried exactly
one: `rename-references` in docs-hygiene and `tidy` in code-tidying, both visible as the
single `{ git status --porcelain 2>/dev/null || echo "(git status unavailable)"; }` brace
form in the plugin. Each entry now names its one site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo
A post-merge security audit of 908902d found the defect that commit fixed for
git-status probes still live in the rest of its class, at sites the sweep never
scoped. The worst of them defeats a documented safety gate.

plugins/knowledge/skills/video-digest/SKILL.md states: "STOP if the pre-computed
context above shows MISSING for yt-dlp, ffmpeg, or ImageMagick. Cloud agents
without the media toolchain fail closed." Its probes were
`yt-dlp --version 2>/dev/null | head -1 || echo "MISSING ..."`. Because `||`
binds to the pipeline and `head` exits 0 whatever the probe did, an absent
yt-dlp rendered an empty string, never MISSING. The gate could not fire. A skill
documenting itself as failing closed failed open. Verified by execution before
and after: absent now renders MISSING, present renders the version.

The same shape, with a fallback naming a failure the reader must act on, is
fixed in course-digest (2 probes), ai-slop:audit, provenance:audit (2),
firecrawl:update, and claude-ops:observability. Each uses the guard-first form
firecrawl:firecrawl already shipped correctly, so the fleet gains no new idiom.
No `$` expansion is introduced: the composed precompute block must stay
verifiable to the worktree-isolation guard, which rules out the capture-first
variant.

Also fixes a grant 908902d left behind. It rewrote the two prototype skills'
injections to begin with `{` while their line-6 grant remained the prefix rule
`Bash(git status:*)`, whose leading token no longer matches. Narrowing, never
widening, but a probe that silently stops running is the same class of silent
failure as a fallback that cannot render.

Sites whose fallback names an empty result rather than a failure -- `none`, `0`
in session-flow:find-handoff, implementation:implement, claude-memory:audit --
are deliberately not touched here; they belong to the filtered-probe pass and
are recorded with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo
@claude
claude Bot merged commit c66f26c into main Aug 28, 2026
59 checks passed
@claude
claude Bot deleted the claude/docs-hygiene-ssot-extraction-jft8sf branch August 28, 2026 09:07
claude Bot added a commit that referenced this pull request Aug 28, 2026
…l-proof (#3469)

#3468 fixed five filtered-probe injections so a failed probe could be told
apart from a filter that matched nothing. The form it shipped is wrong under
set -o pipefail, and wrong in the more damaging direction: it asserts a failure
that did not happen.

  probe >/dev/null 2>&1 && probe | filter | head -10 || echo "(git status unavailable)"

The && list takes the pipeline's exit status. Without pipefail that is head's,
which is 0, and the guard works. With pipefail it is the last non-zero status in
the pipeline, which two ordinary situations produce: grep matching nothing, and
git taking SIGPIPE when head closes the pipe at the cap. Either fires the
failure token on a healthy probe. That is worse than the defect #3468 removed:
the shape it replaced only ever said none, while this one positively asserts
git status was unavailable when it ran fine.

Both modes reproduced against the shipped lines, then shown fixed. On a healthy
repository whose dirty files do not match, and again on one with 3,000 dirty
files, the 0.21.23 form prints (git status unavailable) and this one does not.
Outside a repository this one still prints it, which is the case the token is
for.

The filter pipeline now sits in a brace group closed by :, a command that cannot
fail, so the || is reachable only by the guard short-circuiting. No $ expansion
is introduced; the composed pre-compute block must stay verifiable to the
worktree-isolation guard. audit-comment-residue's parity test anchors its
porcelain capture past '&& {' so it evaluates the data run rather than an
unterminated brace group, and its capture was checked non-empty so the assertion
still bites.

Open and undocumented: whether Claude Code executes ! injections under pipefail.
Neither the precompute convention nor the permissions docs say. The brace-group
form is correct under both settings and the shipped form under only one, so this
is the fail-safe direction regardless.

Refs #3468, refs #3460.
kyle-sexton pushed a commit that referenced this pull request Aug 28, 2026
…date

The 0.2.0 entry presents a Phase 6 measurement whose six figures are
actually Phase 3 ones: 1,347 files / 525 candidates / 482 parsed / 43
declined / 0 expired / oldest 2026-04-08 all reproduce exactly at 33dccc5,
the commit that introduces list-corpus.sh, on a clean tree with the scripts
as they existed there. They were carried forward without re-measuring.

Records the current baseline at 619199e with --as-of 2026-08-28: 1,352
tracked markdown after carve-outs (1,395 considered, 43 declined at path
level), 535 candidates, 491 parsed, 44 declined at stamp level (20
month-name, 24 bare years), 0 expired at the 180-day default, oldest parsed
stamp 2026-04-08, 9 findings at a 60-day window.

Two of those figures expire, so the as-of date is pinned beside the commit:
0 expired holds only until 2026-10-05 on the current oldest stamp, and the
60-day finding count moves daily. A baseline recorded without one repeats
the staleness this entry corrects.

Also corrects the delta's attribution. It is not main moving across #3467
to #3469 — those contribute +1 in total, one file in #3468. #3467 adds 20
markdown files and contributes zero, all of them inside the excluded
fixtures tree, which is why considered rises by 20 and the fixture decline
goes 3 to 23 while the corpus is untouched.

An earlier draft of this entry claimed the figures came from no commit at
all. That replay covered only history reachable from main, which the
squash-merge of #3467 had made exclude the originating build branch; the
reflog held it throughout. The entry now records that a history replay
bounded at a squash boundary cannot answer whether a number came from a
commit. Refs #3465.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tvKPLnWr7suxxAupU4p8A
kyle-sexton pushed a commit that referenced this pull request Aug 28, 2026
…a spaced skill dir

#3469 fixed five filtered probes whose `||` fallback fires spuriously under
`set -o pipefail`, and stopped there. The same shape is still live at eleven
more sites across six plugins, four of them introduced by #3468 -- the commit
immediately before the one that diagnosed the bug class.

The mechanism is unchanged. `guard >/dev/null 2>&1 && probe | cap || echo TOKEN`
takes the pipeline's exit status. Without pipefail that is the cap's, always 0.
With pipefail it is the last non-zero status, which two ordinary situations
produce: a filter matching nothing, and the writer taking SIGPIPE when the cap
closes the pipe. Either fires the failure token on a healthy probe.

ai-slop:audit is the worst and was live, not latent. detect.sh --show-config
emits 10 lines against a head -8 cap, writing one at a time, so head always
closes the pipe mid-run: PIPESTATUS reads 141 0. The probe rendered the full
correct config and then appended "detector unavailable" beneath it. It
reproduced from any directory with no configuration at all.

knowledge is the one with a consequence beyond a wrong string. video-digest
states "STOP if the pre-computed context above shows MISSING for yt-dlp,
ffmpeg, or ImageMagick. Cloud agents without the media toolchain fail closed."
#3468 fixed that gate's fail-open defect and shipped this inversion in its
place, so the gate would refuse to run on a machine that HAS the toolchain.
Whether the token fires is a race between the tool writing its banner and the
cap closing the pipe, so it scales with output size. Measured here over 30 runs
per size against a synthetic tool on PATH, installed and working: 41 lines
fired 29/30, 5 lines fired 19/30, 1 line fired 0/30. So ffmpeg fails on nearly
every run, magick (5 lines) is a genuine race, and yt-dlp (1 line) is latent by
shape only. An intermittent false MISSING is harder to diagnose than a
consistent one; all three are fixed regardless.

Also fixed here, same shape: course-digest's two probes, firecrawl:firecrawl
(pre-existing, and the idiom the fleet copied), provenance:audit's two detector
probes, docs-hygiene:audit-derivability -- whose capture-first form protects
only the git call, leaving its awk-and-head data run exposed past 20 dirty
files -- and code-tidying:tidy, where a bare third "unknown" line was appended
to well-formed output.

Two of the eleven are latent by shape, not observed failures, and the
provenance changelog says so plainly rather than claiming a live fix:
list-corpus.sh --show-config emits 7 lines against a head -10 cap, and
check-stamps.sh is piped into tail -3, which drains its input and cannot raise
SIGPIPE. Both were verified to behave identically before and after in three
states under both settings.

Separately, ${CLAUDE_SKILL_DIR} is now quoted in ai-slop:audit and
provenance:audit. Installed under a path containing a space, the unquoted
expansion made a working detector report itself unavailable. firecrawl:update
already quoted it; these now match. Pre-existing, but these commits are what
made the token reachable at all -- before them the same failure rendered empty.

Quoting changes the literal command string, and Bash permission rules are globs
over that literal string, so the unquoted script grants no longer match the
quoted invocations. Three companion rules are added, one per quoted script:
Bash("${CLAUDE_SKILL_DIR}/scripts/detect.sh":*) in ai-slop, and the list-corpus
and check-stamps equivalents in provenance. The unquoted rules are kept because
both skill bodies still instruct unquoted invocation elsewhere. Each pair names
one script under the same ${CLAUDE_SKILL_DIR} anchor with the same :* argument
scope, so nothing is authorized that the plugin could not already run. No other
grant changed: every other rewritten command keeps its original leading token,
which is why code-tidying:tidy takes the ||-inside-the-brace-group form rather
than the brace-group-first one.

The remedy elsewhere is the brace group #3469 established: the data pipeline
sits in { ...; :; }, closed by a command that cannot fail, so the || is
reachable only when the guard short-circuits. No new idiom, and no $ expansion
introduced beyond the quoted ${CLAUDE_SKILL_DIR}; audit-derivability keeps the
pre-existing $s capture, which is the part that legitimately drives its ||.

Every site proven by execution in three states -- tool or script absent, output
under the cap, output over the cap -- with and without pipefail. The five sites
#3469 fixed were re-verified at 3,000 dirty files under both settings and are
correct as shipped, as is claude-ops:observability, whose sed drains its input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo
claude Bot added a commit that referenced this pull request Aug 28, 2026
…a spaced skill dir (#3474)

#3469 fixed five filtered probes whose || fallback fires spuriously under
set -o pipefail, and stopped there. Two independent verifiers found the same
shape still live at eleven more sites across six plugins, four of them
introduced by #3468 -- the commit immediately before the one that diagnosed the
bug class.

The mechanism is unchanged. guard >/dev/null 2>&1 && probe | cap || echo TOKEN
takes the pipeline's exit status. Without pipefail that is the cap's, always 0.
With pipefail it is the last non-zero status, which two ordinary situations
produce: a filter matching nothing, and the writer taking SIGPIPE when the cap
closes the pipe. Either fires the failure token on a healthy probe.

ai-slop:audit was live and unconditional. detect.sh --show-config emits 10 lines
against a head -8 cap, writing them one at a time, so head closes the pipe
mid-run every time; PIPESTATUS reads 141 0. The probe rendered the full correct
config and then appended 'detector unavailable' beneath it, reproducible from
any directory with no configuration at all.

knowledge had a consequence beyond a wrong string. video-digest states 'STOP if
the pre-computed context above shows MISSING for yt-dlp, ffmpeg, or ImageMagick.
Cloud agents without the media toolchain fail closed.' #3468 fixed that gate's
fail-open defect and shipped this inversion in its place, so the gate would
refuse to run on a machine that HAS the toolchain. Measured over 30 runs each
with the tool installed and working: ffmpeg 29/30 (41 lines), magick 19/30 (5
lines, a race), yt-dlp 0/30 (one line, never reaches the cap). After the fix,
0/30 at every size.

Also fixed: course-digest's two probes, firecrawl:firecrawl (pre-existing, and
the idiom the fleet copied), provenance:audit's two detector probes (latent by
shape only, and its changelog says so rather than claiming a live fix),
docs-hygiene:audit-derivability whose capture-first form protects only the git
call, and code-tidying:tidy where a bare third 'unknown' was appended to
well-formed output.

One grant moved, and an earlier draft of this message denied it. Quoting
CLAUDE_SKILL_DIR changes the literal command string, and Bash permission rules
are globs over that string, so the unquoted rule stops matching the quoted
command. Three companion quoted rules were added with the unquoted ones
retained; same script, same anchor, same :* scope, nothing widened. The quoting
itself fixes a real defect: installed under a path containing a space, the
unquoted expansion made a working detector report itself unavailable.

Roster completeness was re-derived by a method unrelated to the first grep, a
parser tracking brace depth and quote state, which found zero of these eleven
and nine survivors, all correctly-drawn declines or structurally immune.

Open and undocumented: whether Claude Code runs ! injections under pipefail.
The brace-group form is correct under both settings, so the exposure is
avoidable regardless.

Refs #3469, refs #3468, refs #3460.
claude Bot added a commit that referenced this pull request Aug 28, 2026
…five wrong keeps (#3475)

#3468 closed 16 cross-plugin path citations under ADR 0018 and kept 23 on a
test it never wrote down. Its own verifier reported '3 keeps refuted; 4 cannot
verify' and that reasoning was unreadable, because the verifier ran as a remote
session the agent could not read back. An independent verifier settled it: five
are wrong, not three, and it found why this recurred.

The dividing test #3468 applied -- fix a citation that is an ADDRESS for an
obligation, keep one that is EVIDENCE about this tree at a moment in time --
appears nowhere in ADR 0018. Clause 2 reads unconditionally. But ADR 0018 is
itself a docs/** file and path-cites plugin skill privates five times, quoting
their contents inline. Either it violates itself on its own first page, or
clause 2 is scoped to citations that function as an address. Only the second
reading is coherent, and an unwritten rule is what let one commit rule two ways
in two files. This writes it down, as an Amendment following ADR 0011's
convention: the evidence/address test, scripts/lib being outside the
entry-surface carve-out, and how plugin-level non-skill trees generalize.

The five wrong keeps. shell-test-helpers cited a private scripts/lib/ path --
and that is a self-contradiction inside c66f26c itself, which added
detector-findings 2.8.1 fixing the byte-identical shape and stating the ground,
then kept it one file over. Three line and step pins are dropped as the same
class 2.7.1 removed a :414 pin for; each citation already quotes its content
inline, so the drop is lossless. records.json:3's keep-ground was false: the
sweep kept it because generation overwrites hand-edits, but that store's own
note says 'Hand-editable and human-gated'.

Recorded as open remainder rather than swept: twelve citations c66f26c created
itself, every fix having written a changelog entry quoting the path it removed,
so the population is 16 fixed and 35 kept rather than 16 and 23; three trees
excluded by fiat with no carve-out authorizing it; 51 citations into
plugin-level non-skill trees; and a stale dometrain security record whose own
version-bump re-trigger has fired unnoticed.

A fifth review round found two more false claims, both stale statuses. '34 to
32' was wrong: re-deriving all 34 rows against the roster's own text test finds
22 already closed, twelve of them by #3380, the same commit that last wrote the
roster and left its own 'all 34 re-verified' line standing over twelve citations
it had just deleted. This change set republished that status in a file whose
line 18 reads 'the check is the text, never the status and never the line
number.' And the spec still shipped the probe shape #3469 removed, presented as
verified in three states, two of which print the failure token under pipefail.

Refs #3468, refs #3469, refs #3460. Sibling #3474.
claude Bot added a commit that referenced this pull request Aug 28, 2026
…test, and record the pipefail question (#3476)

The last five findings from an independent security review of #3460/#3468/
#3469. #3474 and #3475 closed the probe-shape and citation halves; this closes
the filter half and writes the open question into the convention.

Four probes silently under-reported any filename git status --porcelain
C-quotes. A plain space is enough to defeat grep '\.md$' and awk '{print $NF}'.
Executed against a repo holding seven files per extension (plain, space, single
quote, double quote, semicolon, pipe, embedded newline), each of the four went
from catching 4 of 7 to 7 of 7. Two correct forms already shipped in the same
two plugins, so this was drift rather than a missing idea: the .md sites took
audit-noise's grep form, which keeps their leading token and so keeps their
grant valid, and dissolve-comments took audit-comment-residue's -z NUL parse.
The residual is stated in the changelogs rather than hidden.

audit-comment-residue's parity test could not see an over-reporting preview.
Its loop only checked one direction, so mutating the SKILL.md awk to drop its
rename skip made the preview emit a phantom path while detect.sh audited five
real ones, and the suite still passed 53/53. A reverse loop now asserts every
previewed path is one detect.sh audited: the same mutation fails 1/54 naming the
phantom, and reverted it passes 54/54. The forward loop is byte-identical.

claude-ops:observability rendered 'unknown' both outside a repo and inside one
whose toplevel directory is literally named unknown. The failure case now
renders a distinct token.

Six labels asserted 'empty = none' in two states the probe does not establish:
the brace group's : makes the outer || unreachable, which also masks a
filter-stage failure and a guard/data double invocation. The labels now say
'empty = none matched or the probe returned nothing'. No plumbing restructured;
the ~25 unfiltered probes were left alone because their claim is already true.

The durable fix is in precompute-context.md: whether Claude Code runs !
injections under pipefail is undocumented, the brace group is correct under both
settings while guard && pipeline || token is correct under only one, and the
brace group's own price is that an internal failure renders empty, which is why
the label must carry it. The recheck trigger now covers shell options.

Two derivations unrelated to the review's table both return the same six
filtered probes: the true set is six, four defective, not five and three.
#3474 fixed audit-derivability's SIGPIPE shape hours earlier and left its filter
untouched, so both defects landed on one line; this branch was rebuilt onto
current main and carries both, since committing either alone would have
reverted the other.

70 pipefail cells all exit 0; affected-tests real exit 0 with 17/17 suites;
changelog parity including --check-preserved across 306 headings; 3061 plugin
files validated; markdownlint clean over 173 files. No grant widened or broken.

Refs #3474, refs #3475, refs #3469, refs #3468, refs #3460.
kyle-sexton pushed a commit that referenced this pull request Aug 28, 2026
…, and close the dometrain staleness

#3468 closed the ADR 0018 encapsulation floor by writing a convention CHANGELOG entry
for each fix, and every entry quotes the path it removed. That manufactured twelve new
instances of the shape the pass was sweeping. The sweep record listed them and stated
they were fine; nothing ruled on them one at a time, so the arithmetic (16 fixed and 35
kept, not 16 and 23) rested on an assumption.

Ruled individually against the test #3475 wrote into ADR 0018's amendment. All twelve are
keep-correct and none is edited. Each sits in a dated entry whose claim is what a named
file contained on that date, quoted as the string the entry removed, with the replacing
invocation named in the same sentence, so no reader is sent to any of them for a rule.

Three sub-rulings the class needed are recorded, because a bare "all twelve are evidence"
is the assumption this was meant to replace. The three config-cascade rows quote
plugin-relative forms that resolve against nothing, and the entry says so: clause 3 does
not fire where the entry asserts non-resolution, since making them resolve would delete
the finding. The other nine resolve on disk from their own implied base, checked one at a
time. None carries a line or step pin, the part the amendment says rots first.

Re-derived with a second expression over the fix commit's added lines rather than a scan
of the files at rest. Same twelve, plus one the roster excludes on purpose:
plugins/review/reference/topic-docs.md, a plugin-level non-skill tree that belongs to the
pass the amendment routes it to, and a declared keep rather than a quoted removal.

Separately, MIGRATION-PLAYBOOK's dometrain record was reviewed at 0.1.0 and the manifest
reads 0.2.7, eleven releases later. Whether any of them added a trust surface is what the
re-trigger clause turns on and no record answers it, which takes a review rather than a
reading. The staleness is now stated in place with the re-review logged as owed. The
review itself is deliberately not performed here.

Refs #3468, refs #3475, refs #3460.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo
claude Bot added a commit that referenced this pull request Aug 28, 2026
…, and flag a fired security re-trigger (#3477)

Every one of #3468's ADR-0018 citation fixes wrote a CHANGELOG entry quoting
the path it removed, creating twelve new instances of the shape it was closing.
The population at main is 16 fixed and 35 kept, not 16 and 23. #3475 wrote the
governing test into ADR 0018; this applies it to those twelve so a later
re-derivation does not re-open them.

All twelve are keep-correct, established row by row and anchored on text rather
than line numbers. A dated changelog entry quoting a citation it removed is the
evidence case exactly. No CHANGELOG was edited, so no convention bump is owed.

Three sub-rulings the class needed, now written into the spec rather than left
implicit. config-cascade's three rows are the only clause-3 question in the set:
they are plugin-relative and resolve against nothing, and the entry says so in
the same sentence. Clause 3 governs addresses a doc offers, not strings it
quotes in order to report them broken, and forcing resolution would delete the
finding. The other nine resolve on disk today, verified one path at a time
across eleven distinct targets. No row carries a line or step pin, the part the
amendment says rots first.

One excluded on purpose and recorded so it is not re-opened:
plugins/review/reference/topic-docs.md, a plugin-level non-skill tree the
amendment routes to its own pass, which the entry declares a keep rather than
quoting as removed.

Separately, MIGRATION-PLAYBOOK.md records a dometrain security review performed
at 0.1.0 and states that a version bump adding a new trust surface re-triggers
it. The manifest now reads 0.2.7, eleven releases later. The note added states
the reviewed version, the shipping version, that the ACCEPT below it describes
0.1.0 only, and that the re-review is owed and deliberately not performed here.
It stops short of asserting a trust surface was added: that is the condition the
clause turns on and cannot be settled by reading the page. Two checks were run
to avoid claiming otherwise and neither settles it, which the note says.

The spec's dometrain entry pinned MIGRATION-PLAYBOOK.md:943, a line this
change's own edit would have invalidated. Re-anchored on the record's heading
text. That decay rule has been violated three times across this sweep; this is
the first time it was caught before landing.

Recorded because two earlier claims about it were wrong in opposite directions:
this worker's definition declares the Agent tool and the harness withheld it at
spawn, so nesting is gated by depth realized as pre-launch tool omission. There
is no refusal string because there is no call to refuse.

Refs #3476, refs #3475, refs #3474, refs #3469, refs #3468, refs #3460.
claude Bot added a commit that referenced this pull request Aug 28, 2026
…reopened it three times (#3478)

The L4 roster's Group 2, eight intra-plugin path-form citations, was the last
block anyone believed open. All eight were already closed, by 6c7a103 (#3380)
on 2026-08-26 -- the same commit that wrote the roster. Its own message names
what it did: eight citations written with an implied base of the plugin root
while the real base was reference/, none of which resolved for any reader.

Verified one at a time against the roster's own text test. All eight now read
${CLAUDE_PLUGIN_ROOT}/skills/<skill>/<path> and all seven distinct targets
exist on disk. Zero plugins/** files edited, so no bump and no plugin changelog
entry: nothing needed changing.

The reason four separate passes each re-derived this roster from scratch is one
sentence. #3380 fixed 32 of 34 rows and left its summary asserting that all 34
path:line citations were re-verified on 2026-08-26 and every one resolves --
standing over citations it had just deleted. A fifth-round audit caught that for
Group 1, where 22 of 32 were already closed and twelve of those by #3380 itself.
This closes the same hole for Group 2. The generalizable rule is now in the
sweep spec: a record that fixes findings and updates its own summary in the same
commit must update the summary, or the summary outranks the fix for every later
reader.

The roster's inventory rows are left verbatim, per its own decay rule that the
inventory is the part that cannot be re-derived, and per the precedent that
#3474 and #3475 both closed rows without touching the file. Only an additive
closure stamp is appended.

On whether ADR 0018 reaches this class, the honest answer is that its
encapsulation half does not. Clause 1 legalises intra-plugin citations and names
this exact citing surface. Clause 2 cannot apply, because both files ship inside
one plugin so the runtime absence motivating it cannot occur, and the 2026-08-28
amendment's fix-an-address / keep-evidence test divides clause 2 applications
only. Only clause 3 reaches Group 2, and clause 3 is a resolvability rule rather
than an encapsulation one: had these been open, the remedy would have been path
form and nothing else. The clause that earned its keep is the ADR's own
observation that proximity did not prevent them.

A second derivation making no reference to the roster resolved every citation
token in every plugin-level reference/, context/ and agents/ tree plus every
plugin README against the base its own form implies: 52 tokens, 0 clause-3
failures. Group 3's two anchors were also closed by #3380. 34 closed, 0 open.

No fresh-context verifier reviewed this diff. Nested spawning is unavailable at
this depth: the Agent tool is withheld pre-launch, so there is no call to refuse
and no refusal string. A second mechanical derivation was substituted and every
line reference re-checked against the live tree, which is weaker than a fresh
context and is recorded as such.

Refs #3477, refs #3476, refs #3475, refs #3474, refs #3469, refs #3468.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant