"Audit Claude Code configuration files, including settings.json, settings.local.json, .mcp.json, hooks, plugins, permissions and environment variables, for correctness, security, and drift against current official docs. Use when: 'audit settings', 'check config', 'check for config drift', after a Claude Code update, or when permissions, hooks, plugins, or MCP servers may be misconfigured; pass --fix to apply auto-correctable findings with confirmation."
Resources
5Install
npx skillscat add melodic-software/claude-code-plugins/plugins-claude-config-skills-audit Install via the SkillsCat registry.
Arguments. [--fix] [permissions|mcp|hooks|plugins|issues|all]. default: all
Pre-computed context
Claude Code version: !claude --version 2>/dev/null || echo "unknown"
Purpose
Periodic audit of Claude Code configuration files against current official documentation and the
consuming project's own conventions. Answers: "Are our settings correct, secure, complete, and up to
date?"
Adapting to your environment (graceful degrade)
This skill is self-contained. Where a phase names an adjacent capability, such as a Claude Code
issue-tracking skill or a hook plugin's coverage manifest, treat it as optional: if your setup
provides it, use it; otherwise follow the inline guidance here, which stands on its own.
Project-specific conventions (required permission patterns beyond the baseline, documented reasons for
disabled servers, launcher-script wrappers) come from the consuming repo's own CLAUDE.md (or theAGENTS.md a session reads natively, whether in place of a CLAUDE.md or alongside one) and.claude/rules/. Read them when present;
this skill does not assume them.
Two adjacent skills cover neighboring questions: the sibling audit-automation-gaps skill asks whether the
configured automation SET is the right set (landscape gaps); the audit skill in the claude-memory
plugin audits the instruction layer (CLAUDE.md / a natively read AGENTS.md / rules / auto-memory).
This skill asks whether the
configuration FILES are correct against upstream truth.
Arguments
Parse $ARGUMENTS for:
--fix: Apply corrections automatically (with user confirmation per fix). Without this flag, report-only mode- Scope filter: Limit audit to a single category. If omitted, run all categories
permissions: deny/ask/allow rules, security gapsmcp: MCP server definitions, commands, env vars, connectivityhooks: hook scripts exist, timeouts, matchersplugins: enabled/disabled status, marketplace availabilityissues: recheck known GitHub issues onlyall: run everything (default)
Division of labor: the engine decides, the model judges
scripts/audit-engine.sh settles every row a script can decide without a reading and emits one
JSON document plus a findings file. The model never re-derives a row the engine settled; it reads
the document and does only what needs judgment:
| Engine (deterministic, emitted once) | Model (judgment, on the engine's output) |
|---|---|
A: $schema presence and URL, misplaced mcpServers, personal hooks in the local file, whether each top-level and permissions.* key is documented or deprecated on the fetched settings-reference (the installed binary settles an undocumented one), and the A/consent-receipt label for an undocumented top-level key reference/consent-receipts.json records (gates in validation-categories.md Category A) |
A: nothing |
B: presence of each baseline pattern, deny rules in the local file, blanket Bash(git *), the allow-completeness rows at info, narrowing 3 where a hook plugin ships a coverage manifest, suppression-record matching |
B: narrowings 1 and 2 (a documented exemption, a documented hook convention), narrowing 3 for hooks with no manifest, the consuming repo's extra required patterns |
C: command resolution, ${VAR} syntax, URL shape, enableAllProjectMcpServers, enabled/disabled coverage and name validity |
C: documented reasons for disabled servers, launcher-wrapper conventions |
| D: path resolution and readability, millisecond-shaped timeouts, matcher class and anchoring, placeholder quoting in shell form, duplicates, lever state, cache-versus-loaded divergence | D: whether a timeout is reasonable for its tool, exec-form resolution on a Windows-targeting repo, event validity against the live hooks page |
E: marketplace membership, every false key as an inventory row (a finding only when an enabled plugin depends on it), ORPHAN / RENAME drift, catalog plugins with no entry in any scope as one inventory row per marketplace, keys the drift check did not diff, strict versus plugin.json |
E: the fix for a disabled dependency, orphan-true review, rename confirmation |
F: token-shaped values, documentation status against the fetched env-vars page |
F: whether an undocumented custom variable is justified |
G: the measurement, read from an existing debug log; skillOverrides keys that name a known plugin (inert, warning), colon keys whose prefix names no plugin (skip), and entries in the user dir's settings.local.json (info) |
G: the levers, scoped to the roster's composition, and what an undecided colon key names |
H and I: every value check; the accepted effortLevel and disableDeepLinkRegistration values and the version enforceAvailableModels requires come from the fetched settings-reference |
H and I: nothing, once the Phase 3 fetch confirms the behavior the row rests on |
A row the engine marks skip or not-inspectable is exactly that in the report: never clean.
Config Files
| File | How to read | Notes |
|---|---|---|
.claude/settings.json |
the engine, or jq |
Project-level, checked in |
.claude/settings.local.json |
the engine (structure and the four model and effort keys only) | Commonly deny-listed for the Read tool because it holds tokens. Never echo secret values |
.mcp.json |
the engine, or jq |
Project-level MCP server definitions |
~/.claude/settings.json |
the engine, or the Read tool | User-level defaults (optional; checked when it exists) |
start-directory .claude/settings.local.json |
check-structure.sh (structure only) |
Only when the session's start directory is not the repository root AND a copy is there. A pre-v2.1.211 Claude Code wrote the file to the start directory and the current one still reads what it left; the repository-root copy wins on a shared key, but permission rules from both files stay in effect. The dated record for that boundary is audit-permission-state/reference/criteria.md §Scopes |
managed-settings.json + managed-settings.d/ |
check-structure.sh (structure only) |
Machine-scope managed policy, the highest-precedence layer. OS-specific path resolved by the script (macOS /Library/Application Support/ClaudeCode/, Linux/WSL /etc/claude-code/, Windows %ProgramFiles%\ClaudeCode\). Findings on it are report-only routing; managed policy is the administrator's, never edited by --fix |
| managed policy outside the filesystem | not read | The Windows HKLM/HKCU\SOFTWARE\Policies\ClaudeCode policy keys and the macOS com.anthropic.claudecode managed-preferences domain. check-structure.sh names them so an absent managed-settings.json is never read as "no managed policy deployed", but it does not read them |
.claude/audit-pass.md (three cascade layers) |
the engine | The consumer's suppression record, keyed by finding_id. A finding it carries is reported as suppressed with its reason; only the team layer suppresses, a personal-only entry is reported as not applied, and a malformed entry is reported and never suppresses |
Reading settings.local.json safely
Treat settings.local.json as secret-bearing regardless of deny rules: the engine andcheck-structure.sh report key counts, validity, and the four model and effort values by value,
and never dump its contents. Supplemental jq recipes: context/procedures.md
"Reading settings.local.json safely".
The counts are not guaranteed. Where the project's configuration blocks the read, whether a sandboxdenyRead merged from the baseline Read deny or filesystem permissions, the engine reports the
scope as unreadable and its rows as not-inspectable instead of failing. Record the file as not
inspectable, carry that into the report, and do not reach for another reader to get the counts anyway.
Track progress
For any full audit run (Phases 1-5), keep a phase checklist and tick each phase as it completes,
either in-response or by copying ${CLAUDE_PLUGIN_ROOT}/skills/audit/templates/checklist.md
into wherever the consuming repo keeps working task notes. Phase 5 is SKIPPED in default report-only
mode.
Phase 1: Run the engine
Run the engine once, with the findings file in the topic's memory slice (the memory_dir the
consuming repo binds, default .work/):
mkdir -p .work/claude-config-audit
bash "${CLAUDE_PLUGIN_ROOT}/skills/audit/scripts/audit-engine.sh" --table \
--out .work/claude-config-audit/findings.json \
[--docs-dir <dir of pages already fetched>] [--debug-log <file>]The engine records the installed Claude Code version itself (claude --version, carried asclaude_version); version-gated rows are evaluated against it, and Phase 3.2 compares issue-fix
versions against it. An unreadable version turns those rows into skip, never clean.
It also reads the upstream pages its rows rest on, every run, so a default run needs the network.
It hands the docs index (llms.txt) and each page it needs to the plugin's shared fetcher,${CLAUDE_PLUGIN_ROOT}/scripts/fetch-docs.sh, which resolves the page from a link there and reads it verbatim to a file.
The docs object in the document is the coverage record, built from the fetcher's manifest: the
index and each page with its URL or path, byte count, line count, sha256, content type, read time,
and one state: read; unread, with a reason such as fetch-failed, http-404,unexpected-content-type, not-in-index, off-origin, or redirected-off-origin; or unparsed, for a
settings-reference that downloaded but has no heading for permissions or enabledPlugins (a soft
404, a reshaped page). Only read means the engine decided anything from the page; every row resting
on an unread or unparsed page is not-inspectable. The pages this covers today are settings-reference and env-vars; every
other page is Phase 3's. --docs-dir is optional reuse: a page already fetched there is read
instead of fetched again, and a page missing from it is fetched as usual.
Run it with --json instead when you want the whole document; --table prints the version and
the docs coverage, the findings, each with its paste-ready suppress: line, then the suppressed,
skipped, and not-inspectable rows, then the skill-listing measurement. Exit 0 means no
error-severity finding, 1 at least one, 2 a fatal condition (no project settings, jq
missing). Read the document, not the exit code alone.
Pass --debug-log when you know where this session's debug log is; otherwise the engine looks in the documented locations
(CLAUDE_CODE_DEBUG_LOGS_DIR, a file path despite its name, then the newest file under<user dir>/debug/). A log it found rather than was given settles the row only when the log
names this project.
1.0 Hook inventory
The engine runs scripts/check-hook-coverage.sh itself and carries its result inhook_inventory. That script enumerates settings-declared hooks and the hooks shipped by every
enabled plugin, read from the directory the session actually loads (a directory marketplace's
checkout first, the installed-plugin registry otherwise), plus the levers (disableAllHooks,allowManagedHooksOnly, strictPluginOnlyCustomization when it is true or names "hooks") that
switch hooks off, and any
divergence between the loaded directory and the registry's cache snapshot.
Read the inventory state, not just the rows. complete means every enabled plugin resolved.partial means some plugin did not resolve or a hook config did not parse, and unreadable names
which; for anything a partial inventory could not see, keep the conditional posture in
required-permissions.md "Fail open where the inventory is
incomplete". none means no inventory was taken; treat it as partial for every family.
The inventory never runs a hook. Whether a hook covers a family is decided by the engine only
where the hook's plugin ships a coverage manifest (hooks/coverage.json); every other hook stays
the model's judgment, against the three preconditions in "Narrowing the baseline".
1.1 JSON validity
A scope in state invalid blocks every other row about that file; the engine reports the invalid
file as an error and nothing else about it.
1.2 Structure inventory
Fill the summary table from the engine's scopes and rows, or from check-structure.sh when a
managed layer or a start-directory copy is in play (the engine does not read those):
| File | Valid JSON | Keys | Deny | Ask | Allow | Hooks | MCP Servers | Plugins |
|---|
1.3 Baseline snapshot
Record the engine's summary for before/after comparison if --fix is used: findings by severity,
permission rule counts, MCP server count, hook count, plugin count.
Phase 2: Validate
Load the audit checklist: audit-checklist.md
The engine's rows are settled. Work the judgment column of the division-of-labor table, category by
category; full per-check criteria in
context/validation-categories.md (read it when running Phase 2).
- A, Schema & Structure: engine-decided
- B, Permissions: for each baseline row the engine left at full severity, check narrowing 1 (a documented exemption in the consuming repo's rules) and narrowing 2 (a documented project hook convention); for a hook with no coverage manifest, take narrowing 3 by hand against the three preconditions in reference/required-permissions.md; add any patterns the consuming repo's own rules declare as required
- C, MCP Servers: documented reasons for disabled servers; launcher conventions
- D, Hooks: timeout reasonableness, exec-form resolution on Windows-targeting repos (the four-part record is the Category D checklist row), event validity against the live hooks page
- E, Plugins: for each
dependency-disabledfinding, whether to enable the dependency or disable the plugins that need it; orphan-trueand rename review. The engine mergesenabledPluginsfrom the user, project and local files only (managed settings are not merged) and checks direct dependencies only - F, Environment Variables: whether a variable the engine reports as not on the env-vars page is documented elsewhere or justified by the repo
- G, Skill-listing budget: the levers, scoped to the roster's composition (
skillOverridesreaches project and user skills; plugin skills are managed through/plugin). The engine already reportsskillOverridesentries that cannot take effect: a key naming a known plugin (G/skill-override-plugin,warning) and entries in the user dir'ssettings.local.json(G/skill-override-home-local,info); a colon key it left asskipis yours to name or leave undecided. When the engine reportsnot measured, name the routes (/doctorinteractively, a--debugrelaunch headless) and never report clean - H, Model and effort settings and I, Deep-link registration: engine-decided; Phase 3 confirms the behavior each finding rests on before it is reported
Phase 3: Research & Recheck
The engine already read settings-reference and env-vars in Phase 1 and decided the rows that
rest on them. This phase covers the pages and questions it does not: the pages the citation check
names, model configuration, permission syntax, and known issues.
Read every page in this phase verbatim, not through a summarizer. These pages are long, withsettings-reference and env-vars running to hundreds of KB, and a summarizing fetch truncates,
then reports the rows past the cutoff as absent. So for each page,
run bash "${CLAUDE_PLUGIN_ROOT}/scripts/fetch-docs.sh" --out <dir> <page>... to fetch the pages
into one directory and grep the files, per the
fetch route.
A truncated read supports NO finding. Say so and move on, in either direction: neither "the key is
gone" nor "the key is unchanged" is reportable from a read that may have been cut.
The fetcher writes <dir>/manifest.json beside the pages: per page url, retrieved, sha256,status, content_type, bytes, lines, state, and reason, plus claude_version for the run.
A page it reports unread supports no finding.
Then run the citation check over that directory (it reads pages through the same fetcher):
bash "${CLAUDE_PLUGIN_ROOT}/skills/audit/scripts/check-doc-citations.sh" --docs-dir <dir>It greps every key and sentence this skill's references cite
(reference/doc-citations.tsv) in the fetched pages. A MISS means a
checklist row cites text the page no longer carries, and that row is re-derived before it is used;
a SKIP means the page could not be read and its rows stay unverified this run.
3.1 Official docs check
Keys live on settings-reference; the
settings page carries file locations, precedence, and
the $schema guidance. Compare against both:
- Are there new settings the project should consider adopting?
- Have any settings in use been deprecated or renamed?
- Has permission rule syntax changed?
3.2 GitHub issue recheck
For each issue in known-issues.md, check live status by the first
route that works, and never let a route that does not work block the run:
- The REST endpoint,
gh api repos/anthropics/claude-code/issues/<number>, which some sessions
serve where GraphQL is refused. - The consuming environment's own issue-tracking skill when one is installed
(/claude-ops:known-issuesin this marketplace). - Neither reachable: report the issue as unverified since its last-verified date, taken from
that column of the table, so the report states how old the recorded state is instead of an
unqualified "unverified".
For any issue whose upstream fix has shipped at or below the installed Claude Code version, confirm
the settings-specific workaround is still needed and recommend retiring it if not.
3.3 Model configuration verification
Any category H finding requires a fetch of
code.claude.com/docs/en/model-config confirming the
behavior the finding rests on. Do NOT report a category H finding from this checklist's wording
alone: the fallback-chain cap and the allowlist wildcard rule are upstream-owned and move with the
harness. The accepted effortLevel values the engine already reads from settings-reference.
3.4 Permission syntax verification
Fetch code.claude.com/docs/en/permissions and verify:
- The project's permission patterns match current documented syntax
- No new permission types added (e.g., new tool types)
- Wildcard behavior unchanged
Phase 4: Report
Present all findings as a severity-rated GFM table:
| # | Category | Severity | Finding | Current | Recommended |
|---|---|---|---|---|---|
| 1 | B, Permissions | error | Deny rule placed in settings.local.json, where bug #8961 leaves it inert |
settings.local.json → permissions.deny: ["Bash(rm -rf:*)"] |
Move the rule into settings.json; keep settings.local.json deny empty |
Then three short sections the engine's document already carries:
- Suppressed: each finding the consumer's record retired, with its reason, layer and date, plus
everypersonal-only, not appliedandmalformedentry by name. - Not decided: every
skipandnot-inspectablerow, with the reason. An unmeasured skill
listing, an unreadable local file, and an unfetched page all land here, never in the clean count. - Findings artifact: the path passed to
--out. Its rows carry audit-pass's identity tuple, soaudit-passappends them unchanged and a later run diffs against them. When the model adds a
judgment finding of its own, derive its identity with the engine (audit-engine.sh anchorandaudit-engine.sh finding-id) and append the row in the same shape with"tier": "judged".
For any finding the operator decides to keep, print its suppress: line as a stanza they can paste
into .claude/audit-pass.md (team layer): check, claim, sites with surface andanchor/v1, a non-empty reason, and today's date, keyed by the finding_id the engine printed.
That is the declared route for session posture; the skill never infers posture from the
environment.
Severity guide
| Severity | Criteria |
|---|---|
| error | Security gap, broken config, or enforcement bypass |
| warning | Deprecated syntax, missing best practice, stale issue status |
| info | Enhancement opportunity, new feature available |
Interactive checkpoint
If --fix flag is set: Present findings table, then ask user which items to fix. Do NOT proceed without their response.
If report-only (no --fix): Present findings table and summary. Note which items could be auto-fixed.
"Which findings would you like me to fix? Reply with numbers, 'all', or 'skip'."
Phase 5: Fix (only with --fix flag)
For each user-approved fix:
- Make the edit. Done when the target file carries the change and nothing else in it moved.
- Validate with
jq . <file> >/dev/nullafter each edit. Done when jq exits 0; on a parse error,
revert that edit before touching the next one. - Report what changed, as the file, the key, and the before and after values. Done when every
applied fix has a line in the report.
After all fixes:
- Re-run the engine and present the before/after
summary(findings by severity, rule counts,
server counts) - Verify all config files are still valid JSON
Fixes the skill can apply
Auto-fixable (add $schema, move deny rules from local to project, plugin orphan-false
removal via scripts/fix-plugin-drift.sh --yes, which never adds a key) vs judgment-required (adding a baseline deny
rule, new settings from docs, permission restructure, MCP config, orphan-true removal, heuristic
rename), with the full matrix in context/procedures.md "Phase 5, fixes the skill can
apply". Adding and moving a deny rule are graded apart on purpose: moving one is #8961 placement, while
adding one has to be checked against hook coverage that may already hold and against an ask-gate the
addition would suppress.
Required permission patterns
Category B (Phase 2) checks that the project's .claude/settings.json contains a security baseline of
deny/ask patterns: secret-file Read denies, destructive-git Bash denies, and a git push ask-gate. The
concrete list is in reference/required-permissions.md, which the
engine reads directly, so the list is never transcribed anywhere else. Projects with a stricter
posture declare their additional required patterns in their own rules files. When the consuming repo
documents such a list, include it in the Category B check.
A hook plugin can declare which baseline patterns its guards block in hooks/coverage.json. The
engine takes narrowing 3 from that manifest mechanically: the pattern drops to info with the plugin,
hook, and opt-out levers named, and only while the hook is live (no suppression lever set, plugin
resolved). Without a manifest the narrowing stays the model's reading of the guard's source.
Report the secret-file Read denies with their scope, not as protection: a Read(...) deny covers the
built-in file tools and the Bash file commands Claude Code recognizes, but not a subprocess that opens
the path itself. "Scope of a Read deny" in
reference/required-permissions.md carries the covered /
not-covered split, the ranked remedies, and the platform limit. Carry it into the finding rather than
implying the file is unreachable.
CC settings schema, MCP server shape, hook event names, and permission glob syntax are upstream
invariants resolved against their own official pages when a check needs them, rather than asserted
as fixed patterns here.
Boundary, the bundled update-config skill
Both surfaces handle settings.json files, so a request about settings can mean either.
update-config(bundled skill). Ships with Claude Code rather than as a marketplace plugin.
It edits the matchingsettings.jsonorsettings.local.jsonfor a described change: hooks for
automated behaviors, permissions, environment variables, and hook troubleshooting. The model and
the person can both invoke it.- This skill (marketplace plugin). Audits the configuration that exists for correctness,
security, and drift against current official docs, across settings, MCP, hooks, plugins, and
permissions. It reports only, unless--fixis passed.
Routing. When the bundled update-config skill resolves in this session, prefer it for making
a settings change the person requested; prefer this skill for auditing what is configured. A
request such as "allow npm commands" or "add a hook that runs when Claude stops" is a change, not
an audit.
Mutation gate. update-config writes settings files as its job. This skill writes only in
Phase 5, under --fix, one confirmed fix at a time, and never chains into update-config on its
own behalf.
Availability is never assumed. Bundled skills are gated by settings such asdisableBundledSkills and vary by version and host; this section states what to do when the
surface resolves, never that it is present. The four-part records live in
reference/native-update-config.md.
Next
- A finding is a permission grant that auto mode drops or a hardcoded path:
/claude-config:audit-permission-grants. - The configuration is correct and the question is now what automation is missing:
/claude-config:audit-automation-gaps. - Several audit skills need one reconciled, resumable pass:
/claude-config:audit-pass.
Post-Audit
Consumer workaround inventory
When a settings-affecting upstream issue drives a project-specific workaround (or one becomes
obsolete), record that in the consuming repo's own conventions/rules files. The plugin's bundled
known-issues.md tracks only broadly-applicable upstream issues and is
refreshed via plugin updates, not per-consumer edits.
Suggest frequency
Based on findings count:
- 0 findings: "Config is clean. Recheck after next Claude Code major update."
- 1-3 findings: "Minor drift. Monthly recheck recommended."
- 4+ findings: "Significant drift detected. Consider running after every CC update."