`write-scope.mjs` has existed since M-BUG-41, but only one writer ever called it. Measured 2026-08-12: 9 files under `scanners/` write to disk, 1 imported the gate; 21 command templates, 17 mention a write, 5 call `write-scope-cli`. Five templates paraphrasing one policy is the shape that put the lever table in five copies (#61) — one level up. The defect was never "8 ungated writers = 8 bugs". Four of them write the plugin's own bookkeeping and must STAY ungated: a gate that fires on every run gets switched off, and then it guards nothing. The defect is that nothing declared WHICH, so the question was answered by reading, and answered differently each time it was asked. `tests/lib/write-gate-coverage.test.mjs` makes the answer structural: every writer either imports the gate or holds an EXEMPT entry naming where the bytes land. Seen RED against today's tree before the fix (4 ungated writers), and each of its four assertions was separately seen red against its own defect. Two premises in the plan text were falsified by measuring them first: - `scan-orchestrator` was carried as "plugin-managed, legitimately exempt". `--save-baseline` derives its path from the SCAN TARGET, so `--global` lands `~/.claude/.config-audit-baseline.json` — user-scope, require-ok. It is gated. `lib/baseline.mjs` is the genuinely exempt one. - the first sweep scored 9 writers with a regex that could not match `writeFileSync(`, so `lib/backup.mjs` — a real writer — read as clean. The guard covers sync and async forms, strips comments before matching, and asserts non-emptiness so a regex that stops matching cannot make every other assertion vacuously green (#63, #64). Gated: fix-engine, rollback-engine, campaign-export-cli, scan-orchestrator. All five call sites share ONE reduction, `evaluateWriteTargets` — four copies of classify/strongestGate/dedup is the drift this exists to prevent. `campaign export` still DISCLOSES rather than refuses: cross-repo is by design there, and tightening it into a refusal would break the feature. A dry run is still not a write, so it is never gated (#63). A refusal is a verdict about a config that WAS examined, so it rides in the payload and keeps the 0/1/2 exit contract (#62) — and the verdict now reaches the success payload too, since stderr is discarded by `2>/dev/null` (F3's class). commands/fix.md carries `--approve-scope` from the answer the user gives, with the rule stated where it can be read: classifying is not approving. Dogfooded end to end: a target outside the session root refuses with zero bytes written, then applies under `--approve-scope`. Suite 1703 -> 1707/0. Frozen v5.0.0 + default-output snapshots: 0 changed files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pkn22uGCgk6QZA738zNmHL
211 lines
8.7 KiB
Markdown
211 lines
8.7 KiB
Markdown
---
|
|
name: config-audit:fix
|
|
description: Auto-fix deterministic configuration issues with backup and verification
|
|
argument-hint: "[path] [--dry-run]"
|
|
allowed-tools: Read, Write, Glob, Grep, Bash, AskUserQuestion
|
|
model: sonnet
|
|
---
|
|
|
|
# Config-Audit: Fix
|
|
|
|
Auto-fix deterministic configuration issues. Scans, plans fixes, backs up originals, applies changes, and verifies results.
|
|
|
|
## Arguments
|
|
|
|
- `$ARGUMENTS` may contain:
|
|
- A target path (default: current working directory)
|
|
- `--dry-run`: Show fix plan without applying
|
|
- `--global`: Include user-scope config (`~/.claude`) in the scan **and** the fix run
|
|
- `--raw`: Pass-through to scanners; produces v5.0.0 verbatim envelope (bypasses the humanizer) for byte-stable diff tooling
|
|
|
|
`--global` must be passed to **every** step below. The scan that builds the table and
|
|
the scan that plans the fixes are two different runs; if only one of them sees the
|
|
user scope, the plan and the table describe different config.
|
|
|
|
## Implementation
|
|
|
|
### Step 1: Greet and scan
|
|
|
|
Tell the user:
|
|
|
|
```
|
|
## Config-Audit Fix
|
|
|
|
Scanning for auto-fixable issues...
|
|
```
|
|
|
|
Parse flags and run scanners silently. Default mode emits humanized JSON — each finding carries `userImpactCategory`, `userActionLanguage`, and `relevanceContext` alongside the v5.0.0 fields:
|
|
|
|
```bash
|
|
RAW_FLAG=""
|
|
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
|
|
# Set to --global when the user asked for global scope, otherwise leave empty.
|
|
# A placeholder in square brackets does not start with a dash, so the arg loop
|
|
# would take it as the scan/fix TARGET instead of a flag.
|
|
GLOBAL_FLAG=""
|
|
node ${CLAUDE_PLUGIN_ROOT}/scanners/scan-orchestrator.mjs "<path>" --output-file /tmp/config-audit-fix-scan.json $GLOBAL_FLAG $RAW_FLAG >/dev/null 2>/dev/null; echo $?
|
|
```
|
|
|
|
Exit code 3 → tell user: "Scanner error. Try `/config-audit posture` to check your configuration."
|
|
|
|
### Step 2: Plan fixes
|
|
|
|
Run fix planner silently. The fix-cli emits humanized prose to stderr in default mode and v5.0.0-shape JSON to stdout when `--json` is set; we use `--json` here for structured data and let the humanizer-aware rendering layer (this command's prose output below) supply the plain-language wording from the scan envelope above:
|
|
|
|
```bash
|
|
# Re-assign here: each fenced block is its own Bash call, so the value
|
|
# set in Step 1 is empty by the time this block runs.
|
|
GLOBAL_FLAG="" # --global when the user asked for global scope
|
|
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs "<path>" $GLOBAL_FLAG --output-file /tmp/config-audit-fix-plan.json 2>/dev/null; echo $?
|
|
```
|
|
|
|
Exit codes: 0 = plan produced, 2 = one or more fixes failed (apply step only), 3 = argument or tool error. On 3, show the stderr message — an unknown flag is rejected by design, not silently ignored.
|
|
|
|
Read `/tmp/config-audit-fix-plan.json` using the Read tool. Cross-reference each fix-plan entry against the humanized scan envelope (`/tmp/config-audit-fix-scan.json`) by finding ID to recover the humanized `title`/`description`/`recommendation` plus `userImpactCategory`/`userActionLanguage` for grouping.
|
|
|
|
### Step 3: Present fix plan
|
|
|
|
First classify where the auto-fixable entries write. With `--global` the run
|
|
takes `~/.claude` into the *fix* pass, so machine-wide and project rows land in
|
|
one table; without a marker they read as equally local. Pass one `--target` per
|
|
distinct file in the auto-fixable set:
|
|
|
|
```bash
|
|
node ${CLAUDE_PLUGIN_ROOT}/scanners/write-scope-cli.mjs --target "<file-1>" --target "<file-2>" --repo "$PWD" --output-file /tmp/config-audit-fix-scope.json 2>/dev/null; echo $?
|
|
```
|
|
|
|
Exit 0 = classified; 3 = argument error (show the stderr message). Read
|
|
`/tmp/config-audit-fix-scope.json` and carry each file's `scopeClass` into the
|
|
table below.
|
|
|
|
Show what will be fixed and what needs manual attention. Group by `userActionLanguage` so the urgency phrasing stays consistent with the rest of the toolchain:
|
|
|
|
```markdown
|
|
### Fix Plan
|
|
|
|
**Auto-fixable ({N} issues), grouped by impact:**
|
|
|
|
{For each userActionLanguage bucket in priority order — "Fix this now" → "Fix soon" → "Fix when convenient" → "Optional cleanup" → "FYI":}
|
|
|
|
#### {userActionLanguage}
|
|
|
|
| # | ID | Issue | File | Scope |
|
|
|---|-----|-------|------|-------|
|
|
| 1 | {id} | {humanized title} | {file} | {scopeClass, or blank when "in-repo"} |
|
|
|
|
**Manual ({M} issues — require human judgment), grouped by impact:**
|
|
|
|
{Same userActionLanguage grouping. Render humanized title and recommendation verbatim — the humanizer already produced plain-language strings, do not paraphrase.}
|
|
|
|
| # | ID | Issue | Recommendation |
|
|
|---|-----|-------|----------------|
|
|
| 1 | {id} | {humanized title} | {humanized recommendation} |
|
|
```
|
|
|
|
### Step 4: Confirm with user
|
|
|
|
If not `--dry-run`, ask for confirmation. Render each distinct string in the scope
|
|
payload's `disclosures[]` verbatim first.
|
|
|
|
When `requiresApproval` is false:
|
|
|
|
```
|
|
AskUserQuestion:
|
|
question: "Apply {N} auto-fixes? A backup is created first — you can roll back anytime."
|
|
options:
|
|
- "Yes, apply fixes"
|
|
- "Show dry-run only"
|
|
- "Cancel"
|
|
```
|
|
|
|
When `requiresApproval` is true — which is what `--global` produces, since
|
|
`~/.claude` is machine-wide — the question MUST say so and the safe option MUST
|
|
come first:
|
|
|
|
```
|
|
AskUserQuestion:
|
|
question: "{K} of {N} fixes change configuration outside this project. Apply all {N}?"
|
|
options:
|
|
- "Show dry-run only"
|
|
- "Yes — apply all, including outside this project"
|
|
- "Cancel"
|
|
```
|
|
|
|
### Step 5: Apply fixes
|
|
|
|
If confirmed, apply:
|
|
|
|
```bash
|
|
# Re-assign here: each fenced block is its own Bash call, so the value
|
|
# set in Step 1 is empty by the time this block runs.
|
|
GLOBAL_FLAG="" # --global when the user asked for global scope
|
|
# --approve-scope carries the answer the user just gave to the Step-4 question.
|
|
# The engine runs the same scope gate as Step 3 and withholds a `require-ok`
|
|
# write on its own, so leaving this empty after the user answered "Yes — apply
|
|
# all, including outside this project" makes the run refuse the very fixes they
|
|
# approved. Set it ONLY on that answer — never as a default, and never because
|
|
# Step 3 already classified the targets: classifying is not approving.
|
|
APPROVE_SCOPE="" # --approve-scope when the user approved the outside-project fixes
|
|
node ${CLAUDE_PLUGIN_ROOT}/scanners/fix-cli.mjs "<path>" --apply $GLOBAL_FLAG $APPROVE_SCOPE --output-file /tmp/config-audit-fix-applied.json 2>/dev/null; echo $?
|
|
```
|
|
|
|
Read `/tmp/config-audit-fix-applied.json` with the Read tool to get applied/failed counts and the backup ID. Exit code 2 means at least one fix failed — report it; `failed[]` carries the reason per fix.
|
|
|
|
The payload also carries the engine's own scope verdict. When it reads
|
|
`"status": "refused"` with `"reason": "scope-gate"`, nothing was written: render
|
|
each line of `disclosures` verbatim, then ask the Step-4 question again rather
|
|
than re-running with the flag on the user's behalf. A refusal is a verdict about
|
|
a config that WAS examined, so the exit code stays in the normal 0/1/2 range —
|
|
do not report it as a tool error.
|
|
|
|
### Step 6: Show results
|
|
|
|
Run a quick posture check to measure improvement:
|
|
|
|
```bash
|
|
node ${CLAUDE_PLUGIN_ROOT}/scanners/posture.mjs "<path>" --json --output-file /tmp/config-audit-fix-posture.json >/dev/null 2>/dev/null
|
|
```
|
|
|
|
Use the Read tool on `/tmp/config-audit-fix-posture.json` and take `overallGrade`
|
|
and the score from there. That read is the only source for the numbers below:
|
|
`--json` prints the same envelope to stdout, but 255 KB of raw JSON in the
|
|
transcript to recover one grade is exactly the waste this plugin exists to find.
|
|
|
|
Present results:
|
|
|
|
```markdown
|
|
### Results
|
|
|
|
**{applied} fixed** | {failed} failed | Backup created
|
|
|
|
{If grade improved:}
|
|
Score impact: {old_grade} ({old_score}) → {new_grade} ({new_score}) — **+{delta} points**
|
|
|
|
{If failed > 0:}
|
|
{failed} fix(es) couldn't be applied — run `/config-audit plan` for alternative approaches.
|
|
|
|
**Rollback:** If anything looks wrong, run `/config-audit rollback {backup-id}` to restore.
|
|
```
|
|
|
|
### Step 7: Manual findings
|
|
|
|
If manual findings exist:
|
|
|
|
```markdown
|
|
### Needs manual attention
|
|
|
|
These {M} issues require human judgment:
|
|
|
|
1. **{title}** ({id}) — {recommendation}
|
|
2. ...
|
|
|
|
Run `/config-audit plan` to get a step-by-step guide for addressing these.
|
|
```
|
|
|
|
## Safety
|
|
|
|
- Backup is **mandatory** — every fix creates a backup first, including file renames (the source file is backed up before the rename, so rollback can restore it at its original path)
|
|
- Dry-run by default — user must confirm before changes
|
|
- Verify after fix — re-scans in the **same scope** the fix run used, so a `--global` run is verified against user scope too
|
|
- Rollback always available — `/config-audit rollback <backup-id>`
|
|
- A failed fix is reported, never swallowed — exit 2 plus a `failed[]` entry
|