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
This commit is contained in:
parent
49bae2657f
commit
1543830c52
12 changed files with 821 additions and 9 deletions
|
|
@ -275,6 +275,18 @@ no/corrupt ledger). Read `~/.claude/config-audit/sessions/campaign-export.json`
|
|||
— the first ~12 lines of `document` only, never the whole file, never the raw JSON (UX rules).
|
||||
Ask for explicit approval to write it.
|
||||
|
||||
Showing the path is not the same as saying it leaves this repo. Classify it first:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/write-scope-cli.mjs --target "<targetPath>" --repo "$PWD" --output-file ~/.claude/config-audit/sessions/campaign-export-scope.json 2>/dev/null; echo $?
|
||||
```
|
||||
|
||||
Read that file and render each distinct string in `disclosures[]` verbatim before the
|
||||
approval question. Exporting into another repo is what this command is *for*, so the
|
||||
gate here **discloses and does not refuse** — say that the write lands in a different
|
||||
project and that a `docs/` directory will be created there if it is missing. Do not
|
||||
turn this into a refusal.
|
||||
|
||||
**On approval, write it** (the CLI does the faithful copy — do NOT hand-write the file):
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -65,6 +65,19 @@ Read `/tmp/config-audit-fix-plan.json` using the Read tool. Cross-reference each
|
|||
|
||||
### 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
|
||||
|
|
@ -76,9 +89,9 @@ Show what will be fixed and what needs manual attention. Group by `userActionLan
|
|||
|
||||
#### {userActionLanguage}
|
||||
|
||||
| # | ID | Issue | File |
|
||||
|---|-----|-------|------|
|
||||
| 1 | {id} | {humanized title} | {file} |
|
||||
| # | ID | Issue | File | Scope |
|
||||
|---|-----|-------|------|-------|
|
||||
| 1 | {id} | {humanized title} | {file} | {scopeClass, or blank when "in-repo"} |
|
||||
|
||||
**Manual ({M} issues — require human judgment), grouped by impact:**
|
||||
|
||||
|
|
@ -91,7 +104,10 @@ Show what will be fixed and what needs manual attention. Group by `userActionLan
|
|||
|
||||
### Step 4: Confirm with user
|
||||
|
||||
If not `--dry-run`, ask for confirmation:
|
||||
If not `--dry-run`, ask for confirmation. Render each distinct string in the scope
|
||||
payload's `disclosures[]` verbatim first.
|
||||
|
||||
When `requiresApproval` is false:
|
||||
|
||||
```
|
||||
AskUserQuestion:
|
||||
|
|
@ -102,6 +118,19 @@ AskUserQuestion:
|
|||
- "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:
|
||||
|
|
|
|||
|
|
@ -30,17 +30,39 @@ Find the most recent session with a plan (use the **Glob tool** for
|
|||
`~/.claude/config-audit/sessions/*/state.yaml`, then Read the newest match — Read
|
||||
does not expand `*`). If none: "No action plan found. Run `/config-audit plan` first."
|
||||
|
||||
Use the Read tool on the action plan and count actions. Tell the user:
|
||||
Use the Read tool on the action plan and count actions.
|
||||
|
||||
Now classify where those actions actually write. A plan whose actions target
|
||||
`~/.claude/CLAUDE.md` and a plan whose actions target `./CLAUDE.md` are the same
|
||||
count of actions — presenting only the count made a machine-wide change look
|
||||
identical to a project-local one. Pass one `--target` per distinct file the plan
|
||||
touches (absolute paths, as written in the plan):
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/write-scope-cli.mjs --target "<file-1>" --target "<file-2>" --repo "$PWD" --output-file /tmp/config-audit-implement-scope.json 2>/dev/null; echo $?
|
||||
```
|
||||
|
||||
Exit 0 = classified; 3 = argument error (show the stderr message). Read
|
||||
`/tmp/config-audit-implement-scope.json`. Tell the user:
|
||||
|
||||
```
|
||||
## Implementing Action Plan
|
||||
|
||||
Found {N} actions to execute across {M} files.
|
||||
A backup will be created before any changes are made.
|
||||
|
||||
{For each target whose `gate` is not "silent", one line:}
|
||||
- `{target}` — {scopeClass}
|
||||
```
|
||||
|
||||
### Step 2: Get user approval
|
||||
|
||||
Render each distinct string in `disclosures[]` verbatim before asking — they are
|
||||
already plain-language, and the payload carries them so this template never has
|
||||
to restate what a scope class means.
|
||||
|
||||
When `requiresApproval` is false, ask as before:
|
||||
|
||||
```
|
||||
AskUserQuestion:
|
||||
question: "Ready to implement {N} actions? Backup created automatically — you can roll back with one command."
|
||||
|
|
@ -50,6 +72,19 @@ AskUserQuestion:
|
|||
- "Cancel"
|
||||
```
|
||||
|
||||
When `requiresApproval` is true, the question MUST name the scope, and the
|
||||
safe option MUST come first — a plan that edits machine-wide configuration
|
||||
affects every project the user opens, so the default must not be "proceed":
|
||||
|
||||
```
|
||||
AskUserQuestion:
|
||||
question: "This plan changes configuration outside this project ({K} of {M} files). Proceed?"
|
||||
options:
|
||||
- "Review plan first" (then show the plan file path)
|
||||
- "Yes — change files outside this project too"
|
||||
- "Cancel"
|
||||
```
|
||||
|
||||
### Step 3: Create backup
|
||||
|
||||
Create backup silently, and **print the backup ID** — Step 6 has to tell the user
|
||||
|
|
|
|||
|
|
@ -73,7 +73,22 @@ Agent(subagent_type: "config-audit:planner-agent")
|
|||
|
||||
### Step 4: Present the plan summary
|
||||
|
||||
Read the generated plan and show a concise overview:
|
||||
Read the generated plan, then classify the files its actions target. This summary
|
||||
IS the approval surface — there is no separate confirmation step here, so a plan
|
||||
that proposes writing to machine-wide configuration has to say so where the user
|
||||
reads it. Pass one `--target` per distinct file the plan touches:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/write-scope-cli.mjs --target "<file-1>" --target "<file-2>" --repo "$PWD" --output-file /tmp/config-audit-plan-scope.json 2>/dev/null; echo $?
|
||||
```
|
||||
|
||||
Exit 0 = classified; 3 = argument error (show the stderr message). Read
|
||||
`/tmp/config-audit-plan-scope.json`. If `gate` is not `"silent"`, render each
|
||||
distinct string in `disclosures[]` verbatim directly under the action table, and
|
||||
mark the affected rows — not in a footnote further down, where a user scanning the
|
||||
table would miss it.
|
||||
|
||||
Show a concise overview:
|
||||
|
||||
```markdown
|
||||
### Action Plan Ready
|
||||
|
|
|
|||
|
|
@ -45,7 +45,21 @@ Use the Read tool on each backup's `manifest.yaml` (the list of changes captured
|
|||
### Restore mode (with backup ID)
|
||||
|
||||
1. Read the list of changes from `~/.claude/config-audit/backups/{backup-id}/manifest.yaml` using the Read tool
|
||||
2. Show files that will be restored — ask for confirmation:
|
||||
2. Classify the `original:` paths before showing them. A restore writes to the
|
||||
absolute path recorded at backup time, which may be machine-wide even when the
|
||||
backup was taken from a project — so the file list must be rendered as the
|
||||
absolute originals, never shortened to a repo-relative-looking form that
|
||||
implies the write stays local:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/write-scope-cli.mjs --target "<original-1>" --target "<original-2>" --repo "$PWD" --output-file /tmp/config-audit-rollback-scope.json 2>/dev/null; echo $?
|
||||
```
|
||||
|
||||
Exit 0 = classified; 3 = argument error (show the stderr message). Read
|
||||
`/tmp/config-audit-rollback-scope.json`, render each distinct string in
|
||||
`disclosures[]` verbatim, then ask for confirmation.
|
||||
|
||||
When `requiresApproval` is false:
|
||||
```
|
||||
AskUserQuestion:
|
||||
question: "Restore 3 files from backup 20260403_163045?"
|
||||
|
|
@ -53,6 +67,15 @@ Use the Read tool on each backup's `manifest.yaml` (the list of changes captured
|
|||
- "Yes, restore"
|
||||
- "Cancel"
|
||||
```
|
||||
|
||||
When `requiresApproval` is true, name the scope and put the safe option first:
|
||||
```
|
||||
AskUserQuestion:
|
||||
question: "This restores {K} of 3 files to locations outside this project. Restore all 3?"
|
||||
options:
|
||||
- "Cancel"
|
||||
- "Yes — restore, including outside this project"
|
||||
```
|
||||
3. For each file in the list of changes:
|
||||
a. Read the backup file from `~/.claude/config-audit/backups/{backup-id}/files/{safeName}`
|
||||
b. Write to the original path
|
||||
|
|
@ -60,8 +83,8 @@ Use the Read tool on each backup's `manifest.yaml` (the list of changes captured
|
|||
4. Show result:
|
||||
```
|
||||
Restored 3 files from backup 20260403_163045
|
||||
- .claude/settings.json (checksum verified)
|
||||
- hooks/hooks.json (checksum verified)
|
||||
- /abs/path/.claude/settings.json (checksum verified)
|
||||
- /abs/path/hooks/hooks.json (checksum verified)
|
||||
- .claude/rules/typescript.md (checksum verified)
|
||||
```
|
||||
5. **Report what rollback cannot undo.** A backup only holds files that already
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue