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
197 lines
7.7 KiB
Markdown
197 lines
7.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
|
|
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:
|
|
|
|
```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
|