-
v6.0.0 Stable
released this
2026-08-18 18:58:21 +00:00 | 2 commits to main since this releaseSummary
"Prose is not a contract" — a MAJOR release whose theme is a class of defect rather than a feature
area. Across three sweeps (Q1, Q2, Q_AUDIT) the same shape kept surfacing: a command template stated
an invariant in prose, code on the other side depended on it, and nothing checked that the two still
agreed. The write-scope gate was policy paraphrased in five templates while exactly one writer
imported it. The argv a template built was never checked against the CLI receiving it —
--stale-after 30arrived as a single argument, matched no flag, and the command reported success
about a threshold the user had just overridden. And the data contracts — backup manifests,
state.yaml,scope.yaml— are hand-built by the model and parsed by engines that know one frozen
example. The first two are now enforced in code and tests; the third is measured and ranked in
docs/q-audit-prose-invariants.md, with the recovery path (rollback) at the top as the surface
that runs precisely when the user is already in trouble.Breaking — a finding ID's
{NNN}names the check, not its emission position (M-BUG-28).
IDs are therefore not unique per finding: one check failing in three files emits three findings
sharing an ID, and(id, file, line)is the instance key. Any consumer keying onidalone must
move to the triple.scanners/lib/finding-codes.mjsis now the single authority — an undeclared or
missing code throws, with no counter fallback, because a fallback lets a half-converted scanner
ship IDs that look valid. Retired numbers are never reissued. Frozenv5.0.0baselines mask IDs
rather than re-deriving them.37 commits since 5.13.0. 1752 tests, 0 failing. GAP dimensions 25 → 24 (one
/doctor
duplicate retired). No component-count change: scanners 16, agents 7, commands 21,
hooks 4, knowledge entries 8.Fixed
M-BUG-45—/config-audit knowledge-refresh --stale-after Nwas silently dead under zsh. The
command builtSTALE_AFTER="--stale-after 30"and expanded it unquoted, relying on the shell to
split it into two argv entries. bash does; zsh — the macOS default since Catalina — does not.
The CLI received one argv entry reading--stale-after 30, matched no flag, and fell back to the
90-day default while reporting success: "✓ All 14 register entries were re-verified within the last
90 days" — a true-sounding sentence about a threshold the user had just overridden. Measured:
set -- $STALE_AFTER; echo $#prints 1 under zsh, 2 under bash. The threshold is now passed as its
own quoted argument, and a guard rejects any command template that packs a flag and its value into
one variable.M-BUG-46— four CLIs accepted unknown flags in silence. Noelsebranch at all in the parse
loop, so an unrecognised flag vanished without a trace: a typo'd--ledger-filemade
campaign-clireport confidently on the default ledger instead of the one the caller named, and
a mistyped--stale-afterreverted to 90 days. This is what madeM-BUG-45silent rather than
loud.campaign-cliandknowledge-refresh-clinow fail with exit 3 and name the offending flag;
optimize-lens-cliandtoken-hotspots-clishare the defect and are closed together with their
positional-swallow arm in the v5.14 argument-handling work (tracked in the guard'sKNOWN_OPEN).M-BUG-47— the machine-wide token bill counted repos it could not read.refresh-tokens
routed a repo toskipped[]only whenreadActiveConfigthrew, but that function resolves any
path and its sub-readers all tolerate ENOENT, so a repo that does not exist yields an empty config
instead of an error. Measured: a phantom path landed inswept[]with a 0-token delta,
skipped[]was empty, and the roll-up claimedreposWithTokens: 3for a machine with two real
repos — so the command's own honesty clause ("name those repos plainly so the user knows the bill
omits them") could never fire. Readability is now checked before the sweep.M-BUG-48—campaign addvouched for paths that do not exist.add /finnes/ikkereturned
added: [...]and exit 0, and the phantom row then sat in the backlog permanently. Paths are still
tracked (an unmounted volume is a legitimate reason for a repo to be absent today) but are now
reported separately asaddedUnverified, andcampaign.mdnames them instead of glossing over them.M-BUG-49—posturereported a crash as a passing grade. Its top-level catch set
process.exitCode = 1, while every command in this plugin is told that "codes 0, 1, 2 are normal
(PASS/WARNING/FAIL). Only 3 is a real error". A fatal error was therefore indistinguishable from a
WARNING, and the command went on to Read a payload file that was never written. Measured as the
single outlier: 1 of 14 scanners. Now exits 3.M-BUG-50—knowledge-refreshread one register and wrote another, and the gate saw neither.
Step 6 saidEdit knowledge/best-practices.json— an unanchored relative path — while the CLI reads
${CLAUDE_PLUGIN_ROOT}/knowledge/best-practices.json, which for a marketplace install is the plugin
cache. So a normal user has no such file in their repo at all; in the plugin's own checkout the
command read the cache and wrote the working tree; and step 6.3's validation gate ran the cached
test against the cached register — validating the copy that was not edited, and passing no matter
what was written. Every write step is now anchored, and the command states where the register
actually lives (a marketplace copy is discarded on the next plugin upgrade).- No scanner created its
--output-fileparent directory.saveLedgeralways did; the payload
write never did — an accidental asymmetry across all 13 writers.commands/campaign.mdwrites its
report under~/.claude/config-audit/sessions/, so on a fresh machine — precisely the first run
thatcampaign-cliotherwise handles gracefully withinitialized: false— the write threw ENOENT
and the command's exit-code table reported it as a possibly-corrupt ledger, steering the user away
from the one action that would have helped. All payload writes now go through
scanners/lib/write-output.mjs. M-BUG-40, fifth arm —posturewrote four temp files it could never read back. #49 closed the
$$/cross-block class in four commands, butposture.mdsurvived it, and so did the guard written
to prevent exactly this. The guard compared each$$path against the block that created it, so a
path written once and then read via prose ("Read the JSON output file using the Read tool") had
no second occurrence to flag. Measured live: the scanner wrote/tmp/config-audit-posture-21614.json
from PID 21614 while the next Bash call ran as PID 23772, and the read step had no path to hand the
Read tool at all. The invariant is now blanket — no$$in any temp path in any command file —
which also caughtfix.mdandfeature-gap.md. All five sites now use fixed literal paths, repeated
literally in every step that needs them.M-BUG-43— commands leaked whole JSON payloads into the transcript. Every scanner except
scan-orchestratorwrites its payload to stdout when--raw/--jsonis set, even when
--output-filewas given — and the command templates redirected only stderr. Measured on a real
repo:posture255 182 B,whats-active35 922 B,drift28 316 B,manifest23 825 B,tokens
8 768 B.fixandfeature-gapwere the worst case: both ran posture with--json, never read the
file they wrote, and in practice recovered a single letter grade from a quarter-megabyte dump — in
the plugin that exists to cut token cost. 13 invocations across 10 command files now redirect stdout,
and the two commands that needed the data read it from their output file instead.tokensswallowed two documented flags.--jsonand--with-telemetry-recipewere listed as
recognized flags but never threaded into the CLI call, so--jsonreturned the humanized payload
where the docs promised byte-stable v5.0.0 output (measured: 4/4 findings carried humanizer fields,
and the title read "Your file starts with content that changes between turns" instead of
"Cache-breaking volatile content at top of CLAUDE.md"), while--with-telemetry-recipesilently
produced notelemetry_recipe_path— the very flag the command's own closing tip recommends.M-BUG-42—manifestasked for a field the scanner never emits. The render contract used
{load}; the payload carriesloadPattern. The Load column — which the command's own prose calls
the whole point of the view — would render blank for all 96 rows.posture's headline had the same
shape ({qualityAreaCount}, never emitted) and now takes its count from the humanized scorecard
rather thanareas.length, which counts a Feature Coverage row the table below deliberately excludes.
Removed
-
GAP dimension
No autoMode classifier(D1) — retired as a/doctorduplicate. CC 2.1.226's
/doctorCheck 8 covers auto mode with usage-weighted judgement, and the binding positioning
(README «config-audit vs. the built-in /doctor») forbids carrying a feature whose whole value is
duplicating a/doctorcheck. What is retired is only the "adopt this feature" nudge; the
deterministic side stays untouched — SET still validatesautoModestructure and still flags it
as dead config in shared project settings. GAP dimensions: 25 → 24.The title lived in four tables, not the two the removal was scoped against: the dimension
list, the scoringTITLE_TO_IDmap, the humanizer's static translations, and — the one that
moves a user-visible number — the scoring denominators (TIER_COUNTSt3 8→7,
TOTAL_DIMENSIONS25→24,MAX_WEIGHTED42→41).findGapIdfalls back to'unknown'silently,
so a partial removal would have degraded without failing. A blanket sync invariant now asserts
all four againstGAP_CHECKSrather than checking occurrences pairwise, and each arm was
verified red against its own defect.Utilization shifts accordingly (fixture: 43 → 44).
risk_score,risk_band,verdict,
overallGrade,maturityandsegmentare byte-identical across the change — the dimension was
severityinfo(zero risk weight) and GAP is excluded from the overall grade.Frozen
tests/snapshots/v5.0.0/stays untouched. The removal-twin normalizer
(tests/helpers/strip-retired-gap.mjs, mirroringstrip-added-scanner.mjs) strips the retired
dimension from whichever side still carries it and re-derives GAP IDs — retiring a dimension from
mid-list shifts every later ID by one. The derived utilization figures are dropped from
comparison rather than recomputed, since recomputing them in a test helper would assert the new
arithmetic against itself; they are covered exactly intests/lib/scoring.test.mjs. Re-seeding
the baselines was rejected: it would silently bake in any other drift accumulated across every
scanner those four files cover.
Added
-
Four command-template shape tests (1449 → 1453), each verified to fail before the fix: a blanket
$$ban, stdout-redirect discipline for any scanner invoked with--output-filein raw/json mode,
flag threading from prose to shell, and a render-contract test that checks every{field}against a
live payload generated from a fixture rather than a hardcoded key list, which would drift. -
M-BUG-40— command templates assumed shell state survives between fenced blocks. It does not:
every```bashfence is executed as its own Bash call, in its own process. A variable
assigned in one block is empty in the next, and$$(the PID) differs between calls, so a
/tmp/foo-$$.jsonpath created in one block can never be reconstructed in a later one. The defect
was surfaced by dogfoodingplan+implement, and confirmed at runtime by the planner agent
itself, which reported thatMode: $RAW_FLAG"arrived literally unsubstituted" —--rawwas
documented in both files while being functionally dead. A machine sweep found the same root in
20 places across 9 files, far past the two predicted:$RAW_FLAGreferenced from non-shell
agent prompts (analyze,plan,implement);$TMPFILEreferenced across blocks intokens,
manifest,whats-activeandplugin-health, so each command could not read the file it had just
written;$GLOBAL_FLAGinfix;$TODAYincampaign, which was never assigned in any block
and passed--reference-date ""to a write CLI; and three$$temp paths handed to the Read tool
infix, which expands neither$$nor variables. All now follow the hardeneddrift.mdpattern:
a fixed literal path, or a re-derivation inside each block that needs it. -
implementhanded out a rollback ID it never captured. The backup directory was created with
mkdir -p .../$(date +%Y%m%d_%H%M%S)/, so the timestamp existed only inside a command
substitution, while step 6 promised/config-audit rollback {timestamp}— the one message a user
reads after a bad run. The step now printsBACKUP_IDand substitutes it literally. -
planreported "No analysis results found" for valid sessions. Step 1 pointed the Read tool at
~/.claude/config-audit/sessions/*/state.yaml; Read takes one literal path and does not expand
*, so the lookup failed and the command reported the session as missing. It now uses Glob, and
additionally verifiesanalysis-report.mdexists before spawning the planner agent — a session can
carry a validstate.yamland still be missing its report. -
Phase commands wrote
state.yamlwith two of the four required fields..claude/rules/state-management.md
mandatescurrent_phase,completed_phases,next_phaseandupdated_at;analyze,discover,
implement,interviewandplannamed only a subset. Because the planner agent writes all four,
a follow-up full-file Write naming two deletes the other two — the fields that make an
interrupted run resumable. -
implementdocumented a rollback semantics that does not exist. Its "## Rollback" section
promised to "delete newly created files", whilerollback.mddeliberately leaves them in place and
lists them under "Left in place" (deletion is unimplemented;M-BUG-26remains open). The doc now
mirrors actual behaviour rather than describing a half-restore as clean. -
implementclaimed a score delta with no source, since nothing captured the pre-change grade
before the edits ran, and its implied posture call omitted both--output-fileand2>/dev/null
required by the output rules. It now reports a delta only when a pre-change grade was actually
measured. -
verifier-agentwas instructed to write a report it has no tool to write (tools: Read, Glob, Grep, and "Read-only validation" by design). It now returns findings as its final message and the
command appends them with Bash>>, preserving both the read-only design and the shared-log
append discipline. -
Dead tool name in always-loaded context:
.claude/rules/command-development.mdtaught
allowed-tools: ... Taskwhile every command usesAgent, andinterview.mdcarried two more
Taskreferences.planner-agent.mdalso contradicted itself — line 110 forbids inline file
content while its own output template demonstrated exactly that, pushing plans past the 200-line
budget the same file sets.
Fixed (previously released work)
M-BUG-39— every scanner CLI could truncate its own output when piped.process.exit()
terminates immediately, but Node writes stdout asynchronously when stdout is a pipe, so whatever
is still buffered is discarded.scan-orchestrator.mjsmeasured 246 854 bytes to a file vs
65 536 to a pipe (and 131 072 on another run — the cut point is a nondeterministic flush race),
handing any machine consumer truncated, unparseable JSON that reads like a corrupt file rather than
a cut-off. Reported byorg-ops, whose census pipes our envelope. The whole class is closed, not
just the CLI where it was observable:campaign-cli,campaign-export-cli,campaign-write-cli,
knowledge-refresh-cli,drift-cliandfix-cliall exited the same way on their success paths
and were green only because their payloads happen to fit the pipe buffer today. All 38 sites across
14 files now setprocess.exitCodeand return, letting Node exit once stdout drains — the pattern
self-audit.mjsand llm-security's orchestrator already used.fail()throws instead of exiting so
it keeps its never-returns contract; exit codes andError:/Fatal:stderr text are unchanged.
Guarded bytests/scanners/cli-pipe-integrity.test.mjs: one behavioural test that pipes a >128 KB
envelope, one class sweep overscanners/*.mjs.M-BUG-36—/config-audit drift --listshowed nothing.drift-cli.mjsaccepted
--output-filebut list mode ignored it, and the listing itself goes to stderr, which
commands/drift.mddiscards with2>/dev/null(ux-rules rule 2). The command received 0 bytes
and could render no baselines at all. List mode now honours--output-file;--raw/--jsonstdout
is unchanged and byte-stable. Fourth instance of the stderr-only class afterM-BUG-33.M-BUG-37—/config-audit feature-gappromised a backup it never made. Step 6's
"Create backup" ranfix-cli.mjs <path> --json, but fix-cli is dry-run by default: no backup was
written andbackupIdcame backnull, after which the command edited the user's configuration
believing it could be restored. Passing--applywould have been worse — it executes unrelated
auto-fixes the user never selected. The step now copies the files itself and states plainly that
plain copies are restored by copying them back, not by/config-audit rollback(M-BUG-31class).M-BUG-38—fix-cli.mjssent users to a script that does not exist. After applying fixes it
printedRollback: node scanners/rollback-cli.mjs <id>; there is norollback-cli.mjs— only
rollback-engine.mjs, driven by/config-audit rollback. A dead reference in the one message a
user reaches for after a bad fix. Now points at the command.M-BUG-21(fourth arm) — command templates fed bracketed placeholder flags to the arg loops.
Five templates (config-audit.md,discover.md,fix.md,tokens.md,whats-active.md) carried
literal[--global]/[--full-machine]/[--verbose]inside executable bash blocks. A bracketed
placeholder does not start with-, so every scanner'selse if (!args[i].startsWith('-'))branch
takes it as the scan target — silently scanning a path that does not exist. Replaced with empty
shell variables that expand to nothing when the flag does not apply./config-audit interviewandanalyzenever said which session they act on. Both referenced
{session-id}with no resolution rule, while every other session-aware command globs
sessions/*/state.yamland takes the most recent. Two runs could write to two different sessions.
Both now resolve the session explicitly and exit when none exists./config-audit interviewcould rewind a finished session. The mandated state write had no bound,
so running the optional interview against a session that had already reachedimplementreset
current_phaseand re-added phases. It now appendsinterviewonly if absent and leaves the
furthest phase reached intact./config-audit cleanupinterpolated an unvalidated id intorm -rf. An empty or malformed
{session-id}expands the path tosessions//, deleting every session. The id must now match
^[0-9]{8}_[0-9]{6}$or come verbatim from the directory listing; anything else is refused and
reported./config-audit statusadvertised a command that does not exist. It documented
/config-audit resume {session-id}; there is noresumecommand. Replaced with how session
selection actually works. A test now fails on any/config-audit <word>reference incommands/
without a matching file./config-audit status allwas documented but never parsed. The flag-parse step knew only
--raw. It now parsesalland routes to the all-sessions table.
Fixed (previously)
-
M-BUG-21(third arm) —plugin-health-scanner.mjsswallowed unknown flags, and the wrong
target looked green. The sameelse if (!args[i].startsWith('-')) targetPath = args[i]loop:
--output-file /tmp/x.jsonwas dropped and/tmp/x.jsonbecame the scan target. Wheredrift
produced phantom drift, this produced a reassuring answer — a non-existent path discovers no
plugins, so the scanner reportedNo plugins found(info) and exit0. Unknown options and a
value-less--output-filenow exit3. -
M-BUG-33—/config-audit plugin-healthread zero bytes. The scanner had no--output-file
(ux-rules rule 2) and its default-mode report goes to stderr, whichcommands/plugin-health.md
discards with2>/dev/nullbefore telling the agent to "read stdout output (JSON)". The command's
default path could not produce the report it documents.--output-filenow writes a humanized
payload;--raw/--jsonstdout is unchanged and byte-stable. -
M-BUG-34— the report's per-plugin table and Cross-Plugin section were unbuildable. Per-plugin
data (commandCount,agentCount) and the grade formula never leftscan()— the only grade code,
formatPluginHealthReport, had no caller — and cross-plugin findings were flattened intofindings
behind acategory: 'plugin-hygiene'they share with per-plugin findings. The command mandated both,
so it had to fabricate them. The payload now carriesplugins[](name, declaredName, counts, score,
grade via the sharedpluginGrade) andcross_plugin_findings[](also markedcrossPlugin: true),
via a newscanDetailed();scan()'s frozen v5.0.0 envelope is untouched. -
M-BUG-35—.claude-plugin/marketplace.jsonwas reported as an unknown file. It is the
documented, required location for a marketplace catalog, and a marketplace entry with
"source": "./"makes the repo root its own plugin — such a repo legitimately carries both files.
Genuinely unexpected files in.claude-plugin/are still flagged. -
commands/posture.mddiscarded both optional scanners' output. Its--driftand
--plugin-healthsections randrift-cli.mjs/plugin-health-scanner.mjsin default mode under
2>/dev/nulland read stdout, which is empty in that mode. Both calls now use--output-file. -
M-BUG-21—drift-cli.mjshad no--output-file, and its argument loop turned the missing
flag into a wrong scan target. The loop ended inelse if (!arg.startsWith('-')) targetPath = arg
with no unknown-flag branch, so an unrecognised flag was dropped silently and its value fell
through to the scan target:drift-cli.mjs . --output-file /tmp/x.jsonscanned/tmp/x.json, a
path that does not exist, and reported the resulting near-empty scan as drift — permanently, and
without a warning. The same silence was destructive for--save --namewith the value omitted:
--namewas ignored, the name stayeddefault, and an existing baseline was overwritten.
Unknown options and value-less--name/--baseline/--output-filenow exit3. -
M-BUG-21(second arm) —/config-audit driftcaptured nothing at all.commands/drift.md
ran the CLI under2>/dev/nulland told the agent to "read stdout", but the default-mode report,
the--saveconfirmation, and the--listoutput all go to stderr. All three modes returned
empty.--output-filenow writes the diff (humanized in default mode, raw under--json/--raw,
matchingposture.mjs), and the command reads that file;--savepasses--jsonfor its
confirmation. -
M-BUG-27—driftcompared against baselines anchored to a different directory and called it
"improving".diff-enginenever checked the baseline's storedtarget_pathagainst the current
scan target. Diffing a repo against a baseline saved elsewhere marked every baseline finding
"resolved" and every current finding "new" — a 100% phantom diff surfacing as a reassuring trend,
on the default baseline. The CLI now warns on stderr in every mode and carries
_baselineAnchor {matches, baselineTarget, currentTarget}in the default-mode payload, so a caller
running under2>/dev/nullcan still see it.--json/--rawstdout stays v5.0.0-shaped and the
frozendrift.jsonsnapshot is untouched. -
M-BUG-21(third arm) —fix-cli.mjshad the same unvalidated argument loop, where it moves the
write target. An unrecognised flag was dropped and its value became the scan target, so
fix-cli.mjs <repo> --output-file /tmp/x.jsonsilently audited/tmp/x.json; with--applythe
same slip relocates what gets written. Unknown options and value-less--output-filenow exit3.
--dry-run— documented incommands/fix.md'sargument-hintbut never implemented — is now
accepted instead of silently dropped, and--output-filewrites the fix payload to disk so
commands/fix.mdcan read a file rather than parse stdout it runs under2>/dev/null. -
M-BUG-31—fixpromised a mandatory backup it did not always take.fix-cli.mjsexcluded
file-renamefrom the backup set, so a rule file whose only defect was its extension was renamed
with no backup entry — while the command told the user "every fix creates a backup first" and
handed back abackupIdthat could not restore it. The source file is now backed up like any other. -
M-BUG-32— verification re-scanned a different scope than the fix run.verifyFixeshardcoded
includeGlobal: false. After a--globalrun every user-scope finding fell out of the re-scan and
was therefore counted as verified: a clean "fixed" report for files nothing had touched
(reproduced against an untouched~/.claude/CLAUDE.md). It now inherits the run's scope, and
commands/fix.mdpasses--globalto every step instead of only the display scan. -
M-BUG-29— two fixes on one file were applied in an order that guaranteed failure. A rule file
with bothglobs:and a non-.mdextension had the rename applied first; the frontmatter fix then
failed withENOENT. Renames now sort after every other fix. -
M-BUG-30— critical fixes sorted last.severityOrder[s] || 4mapscritical(weight0) to
4, the opposite of the documented "critical first" contract. The old test used the same falsy
fallback, so it agreed with the bug. Now?? 4. -
A failed fix no longer exits
0.fix-cli.mjsreturns2when any planned fix failed, matching
the0/1/2 = PASS/WARNING/FAIL,3 = errorconvention the other scanners follow.
1420 tests (+10). No count change (scanners 16, agents 7, commands 21, hooks 4).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v5.13.0 Stable
released this
2026-07-31 15:34:09 +00:00 | 40 commits to main since this releaseSummary
"Pipeline hardening" — the batch release of everything found by dogfooding the plugin against the
maintainer's real machine and by walking theanalyze → plan → implement → rollbackpipeline
end-to-end on a throwaway repo copy: one new lens mode (optimize --subtract) and 14 bugs
(M-BUG-11…M-BUG-20,M-BUG-22…M-BUG-25), every one of them a real defect a user could hit.The minor bump is carried by
--subtractalone; the other 14 are fixes. Two themes run through them:
agent-facing commands were scanning config the user cannot act on (plugin-bundled and vendored
copies masking real findings), and new finding types kept shipping without their matching humanizer
entry, so plain-language output contradicted the finding's own evidence. The rollback chunk found the
worst class in the repo: arestoreBackupthat returned{restored: [], failed: []}— a success-shaped
no-op — because nothing agreed on where a backup lives or what its manifest looks like.No count change (scanners 16, agents 7, commands 21, hooks 4). Frozen
v5.0.0snapshots
untouched throughout; the SC-5 default-output snapshot was regenerated once, for two humanized titles
only (M-BUG-15). 1398 tests (+54).Known and deliberately not fixed in this release:
rollbackstill cannot delete files that
implementcreated — a backup cannot hold a file that never existed. It no longer fails silently
(manifests carry acreated:list,restoreBackupreturnscreatedNotRemoved, androllback.md
requires the report), but automatic deletion of user files is destructive and gets its own design.
drift-cli.mjsstill lacks--output-file(M-BUG-21).Added
optimize --subtract— the subtraction axis (BP-SUB-001). Every command so far asked an
addition question: what to add, what to move, what it costs. Nothing asked what no longer earns its
always-loaded rent.--subtractadds that as a fourthlensCheckon the existing hybrid motor — a
mode, not a 22nd command or a 17th scanner, because the measured payoff (~18% of one file) justifies
a mode and no more. It is opt-in and proposes only.
It is also the only lens that proposes removing config, so it carries a guarantee the others don't
need: a load-bearing block is never a candidate. Precision is asymmetric — a missed dead line
costs a few tokens per turn, a wrongly deleted one costs a broken script or a wrong remote — so the
floor is decided in code (scanners/lib/floor-exclusion.mjs) before the opus judge sees anything,
never in prose. That ordering is an invariant, not an implementation detail.
Granularity is the leaf block, with two structural exceptions: a paragraph ending in:merges with
the list it introduces, and an ordered list is a contract whose steps inherit floor from any sibling.
Unordered lists deliberately do not inherit — a load-bearing bullet and a disposable one routinely
share a list.
Verified against a hand-built ground truth written before any classifier existed, with the
comparison machine-checked rather than read by eye: zero load-bearing blocks proposed, 11/18
deletable groups surfaced, ~756 tok ≈ 18% of a ~4300-token file — inside the pre-registered band. The
gate is re-runnable viascripts/dogfood-subtraction-gate.local.mjs. Three bugs the dogfood run
exposed are now covered by fixtures: JS\bis ASCII-only so/\bunngå\b/never matched (every
Norwegian keyword ending inæ/ø/åwas silently dead); a bareword/wordis not a path
("pros/cons" vetoed the largest deletable block); "mid-sentence" must key on a preceding lowercase
letter, or**bold labels:**read as entities and cost 4 of 11 groups.
BP-SUB-001is grounded entirely in the Anthropic steering blog already cited by
BP-MECH-001..004and asserts nothing from the talk that motivated the feature.
Fixed
rollback— the backup path contract the engine and the commands disagreed on
(M-BUG-22/M-BUG-23/M-BUG-24/M-BUG-25). Pipeline step 4 dogfood:/config-audit rollback
could not see a single one of the four real backups on this machine, and reported "Backup not found"
for one sitting right there. Four defects, one root — nothing agreed on where a backup lives or what
its manifest looks like.
M-BUG-22(high):lib/backup.mjsresolved~/.config-audit/backups(pre-v2.2.0) while every
command, agent and doc uses~/.claude/config-audit/backups. The auto-backup hook andfix-cliwrote
to the first,implementto the second,rollbackread only the first — solistBackups()returned
9 phantom backups from the test suite and 0 of the 4 real ones. The canonical root is now
~/.claude/config-audit/backups, with the legacy root kept readable (legacy: true) so older
backups stay listable and restorable.
M-BUG-25(high, the worst failure mode in the file):parseManifestunderstood only the
engine's quotedoriginal_path:spelling, butimplementhand-builds its manifest with
- backup:/original:/sha256:. Every implement-made backup parsed to zero files and
restoreBackupreturned{restored: [], failed: []}— success-shaped, and silent. Both formats parse
now, and a manifest with unparseable entries throws instead of pretending to succeed.
M-BUG-23: both session hooks watched~/.config-audit/sessions, which does not exist — "check
for active sessions" had never fired once. It fires now.
M-BUG-24: the suite calledcreateBackup()against the developer's real home, leaving nine stray
backups there whilecleanupOldBackups()deletes past ten. The root is overridable via
CONFIG_AUDIT_BACKUP_ROOT/CONFIG_AUDIT_LEGACY_BACKUP_ROOT, and both test files use it — any new
test touchingcreateBackup()must too.
Verified against backup20260717_032636on a throwaway copy, through the previously broken engine
path: 3/3 files restored byte-exact (sha256 match), zero writes outside the copy, backup dir
unmodified.rules-validator—globToRegexcorrupted mid-pattern/**/globs (M-BUG-19). The
?→[^/]replacement ran after the{{GLOBSTAR_SLASH}}placeholder was restored to(?:/.+/|/),
corrupting the group opener(?:into([^/]:. Every rule pattern containing a mid-pattern/**/
silently matched only the zero-dir branch, so live rules were flagged "matches no files" (CA-RUL).
Found by dogfooding/config-audit implementon a throwaway repo copy: the implementer agent's
correctposts/**/post.mdrule was flagged dead. Fixture outcomes byte-identical.analyzepersists the agent-returned report (M-BUG-18). The Claude Code subagent harness
instructs spawned agents not to write report/summary/findings/analysis.mdfiles — the parent
reads the final text message. Verified live:analyzer-agentskippedWriteentirely, so
analysis-report.mdnever landed on disk and the plan/interview/status phases found nothing to read.
New orchestrator-writes contract: the agent returns the complete report as its final message and the
analyzecommand saves it verbatim before presenting the summary. The harness note is file-type
specific —planwas dogfooded afterwards and writesaction-plan.mdwithout friction, so the same
fix is not needed there.implementpins>>append discipline on the shared log (M-BUG-20).implement.mdspawns
implementer agents in parallel batches, all appending to the sameimplementation-log.md. Dogfooding
showed agents satisfying "append result to:" with a full-fileWrite— the last writer clobbered 4 of
6 entries. Both contracts now pin the mechanism: append with a Bash>>heredoc, never the Write/Edit
tool on a shared log.optimizelens scopes out plugin-bundled CLAUDE.md + keys candidates by absolute path
(M-BUG-11). The lens CLI fed its precision-gate agent every CLAUDE.md discovery returned, including
the 256 files under~/.claude/plugins/— vendored copies across every cached version plus their
fixtures and examples.optimize --globalproduced 454 candidates across 92 "files", ~250 of them from
plugin-internal files a user cannot act on (the plugin overwrites them on update). Second defect:
candidates were keyed byrelPath || absPath, andrelPathcollides across scopes — a repo-root
CLAUDE.mdand~/.claude/CLAUDE.mdboth key toCLAUDE.md, so the two files that actually matter
merged into one indistinguishable bucket and the agent'sRead(file)would resolve the wrong one.
Dogfood: candidates 454→45, distinct files 92→11, repo vs user-global now distinct.feature-gapscopes presence checks to authored config and reads the settings cascade
(M-BUG-13). The GAP scanner's 25 presence checks ran over the fullincludeGlobaldiscovery, so
this plugin's ownexamples/optimal-setup(vendored across plugin-cache versions) satisfied every
tier-3 check — masking real feature gaps to GAP=0 on any target. And the real
~/.claude/settings.jsonwas invisible to the settings-key checks (theincludeGlobalgotcha plus the
maxFilescap), which would have flippedstatusLine/autoModeinto false positives the moment the
maskers were removed. Both halves are fixed together:isAuthoredConfigexcludes plugin-bundled and
nestedexamples//tests/fixtures/config, andreadSettingsCascadereads user→project→local
directly. Empty target: ~0 (masked) → 18 humanized opportunities.posture --output-filehumanizes findings in default mode (M-BUG-12).feature-gap.mdand
posture.mdboth read findings fromposture.mjs --output-fileand group on the humanizer fields,
butposture.mjsonly humanized the stderr scorecard — its--output-fileJSON wrote the raw
v5.0.0-shape result, so every finding's humanizer fields wereundefinedand both commands silently
degraded to the raw tier-fallback. v5.1.0 plain-language output was dead forfeature-gapand for
posture's finding-level grouping. The payload is now humanized in default mode (applied to
result.scannerEnvelope, which is where posture nests it);--json/--rawstay raw.- AGT findings humanize to "Wasted tokens", not "Other" (
M-BUG-17). The agent-listing scanner emits
an always-loaded per-turn token cost — "the dominant single always-loaded source" — but
SCANNER_TO_CATEGORYhad no AGT entry, so its findings fell through to theOtherfallback, a bucket
that isn't even in the analyzer-agent's category list. All 16 orchestrator scanner prefixes are now
covered by the category map, closing the class. - On-demand copy for the oversized skill-body finding (
M-BUG-16). The v5.11 B7 finding measures a
skill body, which loads only when the skill is invoked — but with noSKL.staticentry it fell
through toSKL._default("using more of the listing budget than it should"), so the humanized title
claimed a listing-budget cost and directly contradicted its own humanized evidence ("loads on demand
only … NOT every turn"). - Honest absence-state copy for two GAP enhancement findings (
M-BUG-15). The "No path-scoped rules"
and "No subagent isolation" checks fire on an empty collection too, but the humanized titles
presupposed the feature exists — "Your subagents share Claude's main work folder" appeared in the same
report as "You haven't set up any specialized helper agents yet". A user cannot simultaneously have no
subagents and have subagents that lack isolation. Both titles now use the house "You haven't set up X
yet" framing, which is honest for the zero-state and the has-but-unconfigured state. Fixed in the
humanizer, not by gating the scanner — a presence gate would have moved the frozen v5.0.0
marketplace-medium baseline. - Size-neutral copy for "CLAUDE.md not modular" (
M-BUG-14). The check is a pure presence check with
no length gate, but the copy claimed the file is "one big block" and that splitting makes it "easier on
the loading time" — an unconditional size overclaim that simply lies for a ~625-token CLAUDE.md. Copy
softened to the honest structural framing; no length gate added, which would have made it the only
size-gated check among its siblings.
Downloads
-
Source code (ZIP)
3 downloads
-
Source code (TAR.GZ)
1 download