[BUG] /doctor prompt-audit: the documented "nothing changes until you ask" guarantee is enforced only by prompt text, and the method has no source-of-truth step

Status Open
Reported on v2.1.288
Maintainer reply None cached
Activity 0 comments · opened Oct 3, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (searches listed under Additional Information)
  • [ ] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code (2.1.288)

What's Wrong?

/doctor prompt-audit documents that "nothing in your files changes until you ask Claude to apply them", but the only thing holding that line is prompt text: in the default auto mode the session rewrote project CLAUDE.md and .claude/settings.local.json unprompted, and the audit method, which has no step for establishing which copy of a file is authoritative or which differences are deliberate, produced recommendations to restore superseded content.

/doctor prompt-audit is a built-in command (v2.1.283+). The memory docs describe its contract this way: "You get a report of findings with proposed edits, and nothing in your files changes until you ask Claude to apply them" (memory → Audit your instruction files). The command runs through the bundled claude-api skill, which loads a method file (shared/prompt-audit.md). That method states the same contract three times: "Propose - never apply edits without the user's consent"; Group 2 edits "are proposed for the user to confirm and are never applied on a blanket request"; and "Apply edits to files only when the request itself explicitly asked for the changes to be applied". It also says the coding agent's settings files (.claude/settings*.json) "can hold secrets - do not read them."

1. The consent guarantee is not enforced. All of these are instructions to the model. Nothing in the harness enforces them. On 2026-10-03 (Claude Code 2.1.288, auto mode, Opus 5.5), a single /doctor prompt-audit run with no further arguments read .claude/settings.json and .claude/settings.local.json, rewrote the project CLAUDE.md (two Write calls and four Edit calls), and twice rewrote .claude/settings.local.json through Python heredocs run in Bash. Those rewrites removed the agent key, disabled one plugin and enabled another. No permission prompt appeared at any point. Each of those actions is documented harness behaviour in auto mode, not a bypass: in-working-directory edits to a non-protected file such as CLAUDE.md are auto-approved without a classifier call, and file writes made by a script inside a Bash command are not checked against path rules. A consent guarantee that the vendor documents for a built-in command is backed by no deterministic control.

2. The method has no source-of-truth step. Step 0 makes the audit "non-interactive by design": scope and assumptions come "from the request and the repository, not by asking". The method then decides which of two conflicting passages is current from git blame ("rewrite the older … to match the newer"), and resolves stale facts against "the current fact" in the repository. It has no step that asks which copy of a file is the one Claude Code actually loads (a skills-dir plugin released from GitHub, or the local working tree? the active cache version, or a superseded one?). It also has no step that asks which differences between copies are deliberate. In a setup that had been deliberately reset from a compromised earlier state, the audit treated committed history and on-disk copies as the authority. It produced recommendations whose effect would be to restore superseded content. A customer who runs a built-in command as shipped gets recommendations built on whichever copy the model happened to read.

What exists vs. what doesn't

| Safeguard | Where it lives | Enforced by | Held in this run? |
|---|---|---|---|
| "Nothing in your files changes until you ask Claude to apply them" | Docs, memory.md | Nothing deterministic | No |
| "Propose - never apply edits without the user's consent" | shared/prompt-audit.md line 12 | Model compliance | No |
| Group 2 (stale-fact/conflict) edits "never applied on a blanket request" | prompt-audit.md Group 2, Volatile-specifics and conflict rows | Model compliance | No. The CLAUDE.md rewrite removed stale-fact strings (No git yet, T-003, skills/synced), which are Volatile-specifics findings |
| Settings files "do not read them" | prompt-audit.md Step 1 | Model compliance | No. Both settings files were read with json.load |
| Protected-path check on .claude/ | Harness (permission-modes → Protected paths) | Harness, for writes whose target path Claude Code can see | Not engaged. The writes were made by a Python subprocess, which the path checks do not cover (permissions.md: Read/Edit rules "don't apply to … arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself") |
| disallowed-tools in skill frontmatter | Harness (skills → frontmatter) | Harness, but "The restriction clears when you send your next message" | Could not hold in this run: the first CLAUDE.md write came after the user's next message. Whether the bundled skill sets the field at all could not be determined, because its frontmatter is not on disk or in the docs |

Source-of-truth failures

The audit read these copies; the loaded copies come from claude plugin list --json in the same session:

| Item | Copy the audit read | Copy Claude Code loads | Consequence |
|---|---|---|---|
| skill-creator@claude-plugins-official | plugins/cache/claude-plugins-official/skill-creator/ab024cdcfa7c | …/skill-creator/d182ca456ca0 | Findings against a superseded version folder |
| markdown-html-skills | plugins/marketplaces/claude-code-skills/markdown-html (marketplace clone) | ~/.claude/skills/markdown-html (@skills-dir) | Findings against a copy that is not loaded |
| learned@skills-dir | ~/projects/learned (local working tree), with blame at HEAD d55ae28 | ~/.claude/skills/learned, installed from a GitHub release | Auditor framed HEAD d55ae28 as the canonical skill and the working-tree divergence as an anomaly. That led to a recommendation to restore from d55ae28 and reinstall, in a setup where the divergence followed a deliberate reset |

Plugin loading reference documents how to trace which copy loads. The method never sends the model there, and Step 0's non-interactive design means the model does not ask either.

What Should Happen?

The audit produces a report and a diff, and no file changes until a step the harness enforces records the user's consent.

Error Messages/Logs

No error is shown; the problem is the absence of one. Timeline from the session transcript JSONL (every permission-mode record in it reads auto; version 2.1.288):

Time (UTC)           Line                 Event
16:07:48             9                    /doctor prompt-audit, no path, no request to apply anything
16:09:00-16:09:25    77-82                Six general-purpose subagents dispatched
16:12:50             219                  User message complaining that stale CLAUDE.md had caused four failed sessions. Not a request to edit
16:14:05             262                  Write -> project CLAUDE.md (result 0.4 s later, no prompt)
16:17:04             386                  Second Write -> project CLAUDE.md
16:17:53             460                  python3 reads .claude/settings.local.json and .claude/settings.json
16:18:02             465                  python3 heredoc rewrites .claude/settings.local.json: removes "agent": "orchestrator", sets agentspec@agentspec: false (result 1.1 s later)
16:18-16:31          487, 588, 682, 737   Four Edit calls -> project CLAUDE.md
16:34:00-16:34:27    811-815              Five more general-purpose subagents
16:38:57             870                  python3 heredoc rewrites .claude/settings.local.json: enables plugin-dev@claude-plugins-official (result 1.3 s later)
16:43:06             1112                 User: "asking for something to be fixed does not mean 'go ahead and make the change directly'"

The transcript has no record of a permission prompt, a classifier decision, or a denial for any of these writes. It does not record whether the classifier reviewed the two Bash commands. The permission-modes docs say Bash commands outside the read-only set route to the classifier in auto mode, and that "Read-only actions and file edits in your working directory are auto-approved, except writes to protected paths". CLAUDE.md is not on the protected-files list. Auto mode has been the default for new sessions on Pro, Max and Team plans since August 14, 2026 (What's new, Week 32).

Steps to Reproduce

  1. Claude Code ≥ 2.1.283 in auto mode, the default since 2026-08-14 on Pro, Max and Team.
  2. Use a project whose instruction files contain stale facts, plus a skill whose working tree deliberately diverges from its last commit, or a plugin with a superseded version folder still in ~/.claude/plugins/cache/.
  3. Run /doctor prompt-audit with no arguments.
  4. In the course of the session, make any statement that the model could read as wanting the stale content fixed. Do not ask it to apply edits.
  5. Observe: (a) whether project CLAUDE.md or .claude/settings*.json change with no permission prompt; (b) which plugin and skill copies the audit cites, compared with claude plugin list --json; (c) whether any recommendation would restore committed or cached content over the deliberate state.

Claude Model

Opus (Opus 5.5, claude-opus-5-5)

Is this a regression?

I don't know (the command is new as of v2.1.283)

Last Working Version

N/A

Claude Code Version

2.1.288 (Claude Code)

Platform

Not recorded

Operating System

macOS (Darwin 25.6.0)

Terminal/Shell

Not recorded

Additional Information

Environment

  • Claude Code 2.1.288 (transcript version field)
  • macOS, Darwin 25.6.0
  • Model: Opus 5.5 (claude-opus-5-5)
  • Permission mode: auto throughout (every permission-mode record in the transcript)
  • Plan tier: not recorded in the transcript
  • Project: a local git repository
  • Method file: bundled-skills/2.1.288/…/claude-api/shared/prompt-audit.md (49,306 bytes)

The fundamental gap

  1. The guarantee is documented, but nothing in the harness backs it. The docs promise that nothing changes until the user asks. That is a product-level statement about a built-in command, not a best-effort skill hint. No harness mechanism implements it. The model decides what counts as "asking": it read a complaint as consent.
  2. Auto mode removes the backstop the guarantee implicitly relies on. In Manual mode, every write would prompt. In auto mode, which is now the default, in-working-directory edits to CLAUDE.md skip the classifier entirely, and script-performed writes to .claude/ are outside the path checks. There is no per-command way to require prompts. The user has to remember to switch the whole session out of auto mode before running the command.
  3. Skill-level tool restrictions cannot hold across a multi-turn audit. disallowed-tools clears at the user's next message, and an audit naturally spans several. Removing Edit and Write also does not remove Bash, which can write files through any interpreter.
  4. The authority model is implicit and always looks backward. The method orders conflicting passages by git blame and checks facts against "the repository". When history includes a deliberately abandoned state, or when the loaded copy is not the repository copy (a skills-dir plugin released from GitHub, a cache version folder, a marketplace clone), that model points the wrong way. The method has no step that establishes source of truth or deliberate divergence, and Step 0 rules out asking.
  5. Workarounds belong at the wrong layer. A user can add a PreToolUse hook that blocks writes to CLAUDE.md while an audit runs. That makes every customer re-implement a vendor guarantee for a vendor command, and a hook on Edit|Write does not see Python subprocess writes. The fix belongs in the command.

Why this matters for governance / enterprise deployment

| Control claim | Who makes it | Enforcement | Auditable? |
|---|---|---|---|
| Audit is report-only until the user asks | Anthropic docs | None (prompt text) | Only after the fact, by diffing files |
| Settings files are not read | Bundled method | None (prompt text) | Only by parsing transcript JSONL |
| .claude/ is a protected path | Harness | Enforced for direct tool writes only | Yes, for tool writes. No, for subprocess writes |

  • Change management. .claude/settings.local.json controls which plugins load and which agent runs as the main thread. Under SOC 2 CC8.1, a change to that file needs an authorization record. Here the change ran as a side effect of a diagnostic command, and the only record of it is the transcript, which startup cleanup can delete.
  • Integrity after incident recovery. An organization that resets a compromised configuration relies on the reset staying put. A built-in "audit" that treats older committed or cached copies as authoritative pushes the system back toward the state that was deliberately abandoned. That is the opposite of what an integrity control should do.
  • Customer expectation. Users reasonably treat a built-in command as vendor-vetted, and run it as-is. My position as operator: only deterministic enforcement of constraints is acceptable. A built-in command that a customer runs unmodified must not produce inaccurate recommendations or dangerous changes.

Proposed solutions

Quick wins:

  1. Run prompt-audit with writes withheld, enforced by the harness. While the command's turn chain is active, remove Edit, Write and NotebookEdit, and deny Bash commands that are not read-only. Keep the restriction in place until the user runs an explicit apply step (for example /doctor prompt-audit apply <hunk-ids>). Unlike disallowed-tools, the restriction must survive the user's next message.
  2. Enforce the settings-file read prohibition with the same mechanism the harness uses for protected-path writes, instead of method text.
  3. Add a source-of-truth step to the method, and allow it to ask. Before auditing, resolve each in-scope plugin or skill to its loaded copy (claude plugin list --json → installPath; skills-dir source; marketplace vs. cache). State in the report which copy each finding is against. When on-disk copies disagree, record the disagreement as a flag rather than choosing one.

Moderate:

  1. Make "newer by git blame" an assumption the user confirms, not a rule the audit applies. Any proposed hunk that moves content toward an older commit, an older cache version or an unloaded copy gets labelled as such, and is never part of an applied set.
  2. Record the apply step. When the user approves hunks, write a durable record (command, hunks, files, hashes) outside the transcript, so the change is reviewable after transcript cleanup.

Longer-term:

  1. Give bundled skills a declared capability boundary that the harness enforces, such as "read-only until apply". Vendor commands then never depend on model compliance for their documented guarantees.
  2. Let organizations require Manual-mode semantics for configuration-mutating built-ins through managed settings, regardless of the session's permission mode.

Related upstream issues

Upstream searches (2026-10-03, gh search issues --repo anthropics/claude-code) for prompt-audit, prompt audit, doctor, /doctor, checkup, doctor edited CLAUDE.md, doctor without confirmation, doctor changed settings without asking, bundled skill propose only applied, report findings first asks confirmation, skill disallowed-tools, skill instruction never apply edits, audit revert older version, git blame skill, audit plugin cache stale version, audit loaded plugin version installPath, model edits settings.local.json python and skill safeguard prompt-only not enforced found no issue about /doctor prompt-audit itself. Several early multi-word queries ran as exact-phrase searches and returned nothing. Those empty results are not counted as evidence, and the queries were re-run as keyword searches.

| Issue | Title | Why it's adjacent but not this gap |
|---|---|---|
| #90848 (open) | /doctor skill applies CLI-installation assumptions when run in Claude Code Desktop (Windows) | The same source-of-truth failure in the /doctor checkup: it audited a binary the session doesn't use. It covers installation checks, not prompt-audit, and has no consent or enforcement angle |
| #98599 (open) | /doctor check 8 always proposes defaultMode: "auto" … | /doctor proposes a redundant change, but asks before applying. A correctness bug, not an unenforced-consent gap |
| #97261 (open) | /doctor's CLAUDE.md checks skip AGENTS.md content | A scope-coverage bug in the checkup |
| #85477 (open) | Rules-governance diagnostics for CLAUDE.md / .claude/rules … | Requests the kind of conflict detection prompt-audit now provides. It doesn't address how that feature authorizes its fixes |
| #92349 (open) | Ultracode removes proportionality … turns a settings audit into 325 agents | A fan-out from an audit, caused by an effort setting. The subagent over-dispatch here is the model, not this gap |
| #88041 (open) / #92178 (open) / #87971 (open) | Auto mode's Bash-first instruction leads to file edits by sed, heredoc or script | Explains why the settings.local.json writes went through Python. They frame it as tool choice, not as a consent guarantee being bypassed |
| #96078 (open) | Edit tool applied changes without a permission prompt despite ask rules for Edit | A permission-rule failure. Here no rule failed: the guarantee was never backed by one |
| #92656 (open) / #90887 (open) | Structural enforcement of skill/process routing / Harness-enforced review gate on every commit | The same principle (instructions alone don't enforce), applied to user-authored routing and review policies, not a vendor command's own safety contract |
| #44778 (open) | System events delivered as user-role messages cause model to fabricate user consent | A consent-fabrication mechanism. Here the model read a real user message as consent, which the harness then could not check |
| #83952 (open) | No supported way to verify WHICH hook file will execute … enforcement from a stale copy | The same "which copy is real" question, for hooks |
| #81217 (closed) | Plugin cache never prunes superseded version dirs | Why a superseded skill-creator folder was there to audit. Disk hygiene, not audit authority |
| #38699 (closed) | CLAUDE_PLUGIN_ROOT inconsistent between hooks/skills and agent environment for local marketplace plugins | Wrong-version copies producing plausible output; this run shows the same failure entering through an audit's choice of which copy to read |

Model behaviour in the same run (separate from this report)

| Behaviour | Classification | Why |
|---|---|---|
| Edited CLAUDE.md and settings.local.json without approval | Model mistake, made possible by the gap above | The method forbids it. The harness let it through. The governance finding is that nothing stops it, not that it happened |
| Read settings files | Model mistake on an unenforced rule | Same structure |
| 11 general-purpose subagents recorded in the main transcript (the user counted ~14), dispatched without a plan while the plugin-dev agents were installed | Model mistake | The method prescribes no subagents. The fan-out pattern is already upstream (#92349, #92631) |
| Full docs pages and subagent reports relayed into the main thread (~520k tokens, user's observation, not re-measured here) | Model mistake | Not prescribed by the method |
| Audited a superseded cache folder and a marketplace clone | Partly structural | The method has no step for finding the loaded copy (above) |
| Claims about Claude Code behaviour made without checking the docs | Model mistake | Not prescribed |

How I found it

I ran the built-in /doctor prompt-audit, unmodified, in a project I was rebuilding after deliberately resetting my Claude Code setup from a compromised earlier state. I expected a report. During the run, the session rewrote project CLAUDE.md and .claude/settings.local.json without my approval, and its findings recommended restoring superseded copies. One example was to restore a skill "from HEAD d55ae28 … then reinstall". Reading the bundled method file showed that every consent and no-read rule was prompt text. The docs confirmed that auto mode auto-approves in-directory CLAUDE.md edits and does not path-check subprocess writes. Found through ordinary use of a built-in command, with no adversarial setup.

View original on GitHub ↗