config-audit/CLAUDE.md
Kjell Tore Guttormsen 1543830c52 feat(commands): a write that leaves the repo says so before you approve it (M-BUG-41)
The chain observed configuration across repos but presented every write it then
proposed as though it landed where the session stands. STATE named two arms;
measuring found five, and two of them are worse than the two already known:

- implement — the approval prompt named NO path at all, only a count, so a plan
  editing ~/.claude/CLAUDE.md and one editing ./CLAUDE.md produced byte-identical
  prompts.
- rollback — the file list rendered `.claude/settings.json`, a repo-relative
  FORM, while the restore writes to the absolute original. The other arms were
  silent; this one pointed the wrong way.
- fix — paths were visible but unclassified, and --global mixed machine-wide and
  project rows into one unmarked table.

The gate's strength comes from the target's scope class, never from the command
asking: five command-owned policies would drift apart the way five copies of the
lever table did. SCOPE_CLASSES is one source for class, gate, wording and
predicate; templates render `disclosures[]` from the CLI instead of restating
what a class means.

Two orderings in that table are load-bearing, and both were measured:

- plugin-managed before user-scope. Both ~/.claude/config-audit/ and the legacy
  ~/.config-audit/ are live, and every command writes session state there. The
  other order fires the gate on every write ever made and gets it switched off,
  which is worse than no gate.
- user-scope before cross-repo. ~/.claude/.git EXISTS, so a plain .git-upward
  walk answers "another repo" for ~/.claude/CLAUDE.md and silently downgrades
  the strongest gate on the subtraction axis's primary target to disclosure.

disclose is not require-ok: campaign export is cross-repo by design, so the gate
there says so rather than refusing. Distinct from require-target-dir.mjs, which
asks whether a scan ROOT is readable (exit 3) — a different invariant, left
unmerged along with its four inline copies.

Also structural, both found while building this: the hand-maintained GUARDED
list in the unknown-flag sweep now derives its completeness from the directory
(measured complete at 14 of 14 first, so nothing was hiding — but the 15th CLI
would have been swept by nothing); and prose shape-guards use whitespace-
tolerant patterns, after one went red against a command file that did say the
right thing, line-wrapped.

Gated: implement, fix, rollback, plan, campaign export. Suite 1596 -> 1625/0,
frozen v5.0.0 and default-output baselines 0 changed files. No new GAP dimension,
no lever, no finding code — utilization denominators untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013941cEohSD5Aw56FVAtBgZ
2026-08-10 05:40:12 +02:00

10 KiB

Config-Audit Plugin

Claude Code Configuration Intelligence — know if your config is correct, find what could improve it, fix it automatically. Three pillars: Health (deterministic scanners), Opportunities (context-aware recommendations), Action (auto-fix with backup/rollback).

Per-command flags, patterns, and feature lists live in README.md and /config-audit help. This file carries what's invariant for working on the plugin.

Positioning vs. built-in /doctor (measured 2026-08-03, binding): we are the deterministic/reproducible/all-scope/zero-quota side; /doctor is usage-weighted one-shot judgment. Never build a feature whose whole value is duplicating a /doctor check — see README «config-audit vs. the built-in /doctor» and docs/v5.13-model-routing-effort-deadref-plan.md §A.

Commands

Core (just run /config-audit to get started)

Command Description
/config-audit Full audit with auto-scope detection
/config-audit posture A-F health scorecard (10 quality areas)
/config-audit tokens Prompt-cache-aware token hotspots, each tagged with its load pattern; cache-aware
/config-audit manifest Ranked table of every token source + always-loaded subtotal
/config-audit feature-gap Context-aware feature recommendations grouped by impact
/config-audit optimize Mechanism-fit lens (procedure→skill, lifecycle→hook, path→rule, never→permission). Agent-driven, not byte-stable. --subtract adds the subtraction axis (what no longer earns its always-loaded rent, BP-SUB-001) — opt-in, proposes only
/config-audit fix Auto-fix deterministic issues with backup + verification
/config-audit rollback Restore configuration from backup
/config-audit plan Create action plan from findings
/config-audit implement Execute plan with backups + auto-verify
/config-audit help Show all commands

Additional

Command Description
/config-audit drift Compare current config against saved baseline
/config-audit plugin-health Audit plugin structure, frontmatter, cross-plugin coherence
/config-audit whats-active Read-only inventory of active plugins/skills/agents/MCP/hooks/CLAUDE.md (with token estimates, and model/effort per agent)
/config-audit knowledge-refresh Refresh the best-practices register (stale check + web poll). Human-approved writes; not byte-stable
/config-audit campaign Machine-wide audit ledger + token bill across repos. Human-approved writes; not byte-stable
/config-audit discover Run discovery phase only
/config-audit analyze Run analysis phase only
/config-audit interview Gather user preferences (opt-in)
/config-audit status Show current session state
/config-audit cleanup Clean up old sessions

Agents

Agent Role Model Color Tools
scanner-agent Find config files sonnet cyan Read, Glob, Grep, Write
analyzer-agent Generate report sonnet blue Read, Glob, Grep, Write
planner-agent Create action plan opus yellow Read, Glob, Write
implementer-agent Execute changes sonnet magenta Read, Write, Edit, Bash, Glob
verifier-agent Verify results sonnet purple Read, Glob, Grep
feature-gap-agent Feature recommendations opus green Read, Glob, Grep, Write
optimization-lens-agent Mechanism-fit precision gate opus orange Read, Glob, Grep, Write

Hooks

Event Script Purpose
PreToolUse auto-backup-config.mjs Backup config files before Edit/Write
PostToolUse post-edit-verify.mjs Verify after Edit/Write, block on new critical/high
SessionStart session-start.mjs Check for active (unfinished) sessions
Stop stop-session-reminder.mjs Remind about current session phase

Reference docs (read on demand)

  • docs/scanner-internals.md — scanner inventory, lib modules, action engines, knowledge base, per-scanner/per-block implementation notes (design rationale, primary-source verification, byte-stability lessons)
  • docs/humanizer.md — plain-language output (v5.1.0), humanizer vocabularies, output modes

Plain-Language Output (v5.1.0)

Default output of all commands routes through humanizeEnvelope (lib/humanizer.mjs), decorating each finding with userImpactCategory, userActionLanguage, and relevanceContext. --raw and --json bypass the humanizer for byte-stable v5.0.0 output. Full detail: docs/humanizer.md.

Suppressions

Create .config-audit-ignore at project root — one exact ID or glob per line (CA-SET-003, CA-GAP-*). Suppressed findings are tracked in the envelope's suppressed_findings for audit trail. Disable with --no-suppress.

Architecture

Workflow: /config-audit → discover + analyze (auto) → plan → implement → verify. Auto-detects scope from git context; override with full|repo|home|current; --delta for incremental. Session state lives under ~/.claude/config-audit/sessions/{id}/ (scope.yaml, discovery.json, state.yaml, findings/, analysis-report.md, action-plan.md, backups/, implementation-log.md).

Finding ID format: CA-{SCANNER}-{NNN} — e.g. CA-CML-001, CA-SET-003, CA-HKV-002, CA-RUL-005, CA-TOK-005, CA-CPS-001, CA-SKL-001, CA-OST-001, CA-OPT-001, CA-AGT-001.

GAP dimensions vs. levers (invariant). GAP_CHECKS holds the 24 dimensions — always evaluated, always counted in the utilization denominators (TIER_COUNTS / TOTAL_DIMENSIONS in scoring.mjs, and TITLE_TO_ID there). A lever is a finding the scanner emits after the loop and only under a measured condition; it carries no tier, never enters those denominators, and is registered in the exported LEVERS object (code + title in one place, because the finding-code guard needs the code and the humanizer-coverage guard needs the title). Adding a dimension moves every user's utilization score and can flip the reported segment in a frozen baseline — adding a lever cannot. When a check is only meaningful for configs that already have some feature, it is a lever.

{NNN} names the CHECK, never the emission position (invariant). scanners/lib/finding-codes.mjs is the single authority: every finding() call passes a code, and an undeclared or missing one throws — there is no counter fallback, because a fallback lets a half-converted scanner ship IDs that look valid. Adding a check takes the next free number for that scanner, never the next source-order position; removing one moves its key to RETIRED_CODES and its number is never reissued. 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 that fix-engine verification uses. Frozen v5.0.0 baselines mask IDs (tests/helpers/mask-finding-ids.mjs) instead of re-deriving them; the check→number pairs are pinned exhaustively in tests/lib/finding-codes.test.mjs.

Conventions

Enforced conventions live in .claude/rules/ (auto-loaded as project instructions):

  • ux-rules.md — output/narration/formatting for all commands (never dump raw JSON, narrate before each step, space-separated command suggestions)
  • command-development.md — required command frontmatter + plugin:action naming
  • agent-development.md — agent frontmatter + "when to use" conventions
  • state-management.md — update state.yaml after every workflow phase

Coding style: scanners are zero-dependency Node ESM; new findings use the CA-{SCANNER}-{NNN} ID format; byte-stable CLIs are verified against frozen tests/snapshots/v5.0.0/ baselines.

Write-scope gate (invariant). Every write target is classified by scanners/lib/write-scope.mjs before it reaches an approval surface, and the scope class decides the gate's strength — never the command asking. Five command-owned policies would drift apart the way five copies of the lever table did. SCOPE_CLASSES is the single source for class, gate (silent/disclose/require-ok), wording and predicate; templates render disclosures[] from write-scope-cli.mjs rather than restating what a class means. Two orderings in that object are load-bearing and were measured, not reasoned about: plugin-managed before user-scope (both ~/.claude/config-audit/ and legacy ~/.config-audit/ are live, so the other order fires the gate on every session write and gets it switched off), and user-scope before cross-repo (~/.claude/.git exists, so a plain .git-upward walk calls ~/.claude/CLAUDE.md merely "another repo" and silently downgrades the strongest gate). discloserequire-ok: campaign export is cross-repo by design, so tightening it into a refusal breaks the feature. Distinct from the require-target-dir.mjs guard, which asks whether a scan root is readable (exit 3) — a different invariant, not to be merged.

Subtraction floor (invariant). optimize --subtract is the only lens that proposes removing config, so scanners/lib/floor-exclusion.mjs runs as a deterministic pre-step before the judge — a load-bearing block is never a candidate, and that guarantee must not be moved into the agent prompt. Two rules follow from it: (1) staleness is not a deletion signal — an outdated version pin inside a floor block is a drift/CA-CML dead-reference concern; (2) tier 2 ≠ tier 3 — a compensatory block that keeps earning its place returns, and reporting it as dead weight is wrong even when the label matches. Norwegian keywords need the Unicode boundaries in subtraction-prefilter.mjs; JS \b is ASCII-only, so /\bunngå\b/ silently never matches.

Testing

node --test 'tests/**/*.test.mjs'

Test fixtures in tests/fixtures/. Per-scanner and per-build-block implementation notes (design rationale, primary-source verification, byte-stability lessons) live in docs/scanner-internals.mdImplementation notes.

Gotchas

  • Session directories accumulate — use /config-audit cleanup to manage
  • Scanners run on Node.js ≥ 18 (uses node:test, node:fs/promises)
  • Plugin CLAUDE.md files in node_modules should be excluded via scope