config-audit/commands/fix.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

7.7 KiB

name description argument-hint allowed-tools model
config-audit:fix Auto-fix deterministic configuration issues with backup and verification [path] [--dry-run] Read, Write, Glob, Grep, Bash, AskUserQuestion 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:

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:

# 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:

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:

### 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:

# 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>" --apply $GLOBAL_FLAG --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.

Step 6: Show results

Run a quick posture check to measure improvement:

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:

### 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:

### 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