Purpose
Answers four questions about a Claude Code installation, and refuses to answer a fifth.
- What is actually here? Every entry labelled automatically as an authored surface or a bulk tree, so a ~100k-file tree does not drown a ~150-file answer. Per-file rows live in the CSV.
- What does the product already manage? Claude Code runs its own retention sweep. Recommending a manual prune of a path it owns generates churn, not space.
- For each number in a filename. What IS that number? A liveness lookup against a TCP port or a
shell
$$returns a clean, confident, wrong "dead." - Is this tree in a deliberate or experimental state? If it is, "looks like decay" is the wrong reading of almost everything.
The fifth question, so what should I delete?, is deliberately not answered. This skill is
report-only and never writes to the target tree. Deletion belongs to /disk-hygiene:clean, and
shedding project state belongs to claude project purge.
Scope boundary
| Question | Owner |
|---|---|
| Is this machine's Claude Code install directory healthy? | this skill |
| Are a repo's project-scope config FILES correct? | /claude-config:audit |
| Are permission grants portable and durable? | /claude-config:audit-permission-grants |
| Is the plugin fleet current, and at what scope? | /claude-ops:plugins audit |
| Delete a genuinely unmanaged leftover | /disk-hygiene:clean |
Full rationale for every scope decision, and where each finding hands off: reference/scope-and-handoffs.md.
Boundary, the bundled doctor skill
One native Claude Code surface asks a question that sounds like this skill's, and the two are routinely conflated:
doctor(bundled skill, aliascheckup). Ships with Claude Code rather than as a marketplace plugin. It health-checks an installation and offers to fix what it finds: installation problems, unused extensions, duplicated or bloated memory files, slow hooks, updates, permissions. It also estimates what the skill listing costs in context. It is the one bundled skilldisableBundledSkillsdoes not remove;DISABLE_DOCTOR_COMMAND=1or askillOverridesentry hides it instead. Basis for all four: the/doctorrow on https://code.claude.com/docs/en/commands names the/checkupalias and the fix-in-place behavior, and https://code.claude.com/docs/en/skills carries thedisableBundledSkillsexemption and both hiding mechanisms. Verified 2026-09-06 against Claude Code 2.1.263 and those two pages as fetched that day. Recheck when either page stops carrying the row, or a release note names bundled-skill gating or thedoctorsurface.- This skill (marketplace plugin), the deep read-only inventory of the install tree: every file classified, product-managed retention separated from genuinely unmanaged state, filename schemes resolved before any liveness check, and a deliberate-or-experimental state detected before anything is called stale.
Routing. When doctor resolves in your session, prefer it for the quick health pass and for
anything you want fixed in place. Prefer this skill when the question is what is actually in this
tree, and what does nothing manage, the classification, the evidence tags, and the per-file CSV
have no native counterpart. Its sibling /claude-ops:audit-performance owns the timed
slowness-capture lane against the same native surface; that description is not repeated here.
Mutation gate. doctor mutates: fixing is its point. This skill's contract is report-only, so
never chain into a doctor fix on this skill's behalf. Surface the finding, and let the user
invoke the fix themselves.
Availability is never assumed. Bundled surfaces are gated on settings and environment, plan,
platform, and host surface, so a session where doctor does not resolve is an ordinary session,
not a broken one. Nothing here depends on it being present: the read-only inventory is complete on
its own.
Never read
.credentials.json, daemon/control.key, daemon/pipe.key, ide/*.lock (its body carries an
authToken), and the values inside ~/.claude.json. These are inventory line-items. Name, size,
mtime, and nothing more. This rule is inherited by every subagent this skill dispatches; say so
explicitly in any prompt you fan out.
The engine enforces it in its reader rather than at each call site, and its entire content-read
allowlist is three files: settings.json, .last-cleanup, plugins/.last_inuse_sweep. Everything
else in the tree, including every file a sibling plugin deposited, is stat-only.
Run it
python3 "${CLAUDE_PLUGIN_ROOT}/skills/audit-install-state/scripts/install_state.py" \
--samples 3 --csv ./claude-install-listing.csv > ./claude-install-report.json
Write both artifacts outside the target root. ${CLAUDE_PLUGIN_DATA} resolves to
~/.claude/plugins/data/<id>, which is inside the tree being scanned; a report written there gets
counted and classified by the run that created it. If a destination inside the root is unavoidable,
pass it as --csv and the engine excludes it from its own scan set and records that under
self_excluded.
Always pass --csv. It is the only artifact carrying per-file rows at all. The JSON is a
summary: one line per top-level entry, never a file listing. Without --csv the run wrote no
per-file listing anywhere, csv.path is null, and the report must not be described as covering
every file. --authored-threshold decides only which entries the JSON labels listing: "per-file" (small enough to be a hand-authored surface worth reading file by file in the CSV)
versus listing: "rolled-up"; it embeds no per-file rows either way and never shrinks the CSV.
Useful flags: --root <path> (else $CLAUDE_CONFIG_DIR, else ~/.claude) · --samples N (default
2; use 3+ on a busy machine) · --authored-threshold N (default 200). Python 3.11+ is the only
requirement. No PowerShell, no third-party packages.
Phase 1. Read the deliberate-state result FIRST
Before reading any other part of the report, read deliberate_state and deny_roots.
A non-empty deliberate_state means a revert ledger (RESTORE.md, PLAYBOOK.md, restore*.py, or
a manifest.json / baseline shallow under plugins/data/) was found. Its directory subtree is
deny-listed and every entry under it reports deny-listed instead of a staleness verdict.
Treat a deny-listed subtree as the possible sole copy of somebody's revert path. Do not propose
anything for it. Check the ledger's age_days, since live versus abandoned changes everything, and
diff against the stored baseline rather than believing the ledger's own summary. A ledger's
self-description is written by the experiment it describes and can be wrong about its own key count.
Phase 2. Retention, before any staleness claim
Read retention. Report effective_days together with its effective_evidence: measured when a
file supplied it, documented-default when nothing did and upstream's 30-day default applies.
The highest-severity finding this skill produces lives here: an unparsable settings.json pauses
the retention sweep, so nothing is being cleaned until it is fixed. If
settings-unparsable-pauses-sweep appears in retention.findings, lead the report with it and
treat every staleness reading below as suspect.
.last-cleanup advancing is direct evidence the sweep ran. If it advanced during your scan, say
so, the tree was not quiesced.
Phase 3. Read the entries
Each entry carries surface, a reading with its own evidence, and file_count_sampled as
{min, max, n}.
| Reading | What it means |
|---|---|
product-managed-healthy |
Every file is inside the retention window. Do not hand-prune |
age-exceeds-window |
evidence: inferred. Some mtimes exceed the window, a measurement, not proof the sweep is failing. Read the why, which names the sweep's documented unit for that path |
keep |
Documented as retained, authored by you, session-scoped, or secret-bearing |
unclassified-report-only |
evidence: no-upstream-row. No documentation covers it, so no retention claim is available in either direction |
deny-listed |
A revert ledger is in this subtree. Nothing here is a candidate |
age-exceeds-window is the reading most likely to be misread. file-history/ retains by checkpoint
count and keeps each file's first snapshot regardless of age; subagents/ and tool-results/ age
out with their parent transcript; session-env/, tasks/ and debug/ are per-session. Old mtimes
on those paths are the documented behaviour.
Per-path retention rules: see reference/surfaces.md.
Phase 4. Numeric names and liveness
numeric_names carries counts by (meaning, liveness), every PID-typed row, and a sample of names
whose scheme is unrecognised.
Everything that is not pid reads not_applicable by construction, not because a lookup
missed. That distinction is the whole point:
ide/<n>.lock, the number is a listening TCP port. The real PID is in the body, which is not opened. A process lookup on the port number returns a clean miss, and deleting on that miss breaks a live IDE session.rate-limit-guard/*.tmp.<n>. MSYS2$$, not an OS PID. Judge by age and zero length.shell-snapshots/...,backups/.... Epoch milliseconds. No PID anywhere in the name.- unrecognised. Reported as
unknown. A scheme the table has never seen fails closed.
alive is a measurement about a process with that id; PIDs get reused, so "therefore this file is
in use" is a further inference. A probe that could not run reports unverified, never dead.
Name schemes and their liveness meanings: see reference/name-schemes.md.
Phase 5. Home-root state
~/.claude.json lives in the home directory, not under ~/.claude, and no value of
cleanupPeriodDays touches it. Report its size and mtime; never its values (MCP server configs can
carry tokens). The supported remedy for its growth is claude project purge <path>. It prints the
full plan and confirms before removing anything, and --dry-run previews it.
.claude.json.tmp.<n>.<hash> siblings are failed atomic-write remnants whose number looks like a
PID and has never been verified, so the engine marks it unknown and attempts no lookup.
Phase 6. Report
Reproduce every claim with the evidence tag the engine attached. Do not paraphrase a tagged claim
into an untagged sentence. That is precisely the step that turns an inference into an apparent
observation for the next reader.
Two output rules that are not negotiable:
- Ranges, never a central tendency, for anything time-varying.
411–413 files, n=3, never~412. Iffile_count_sampledcarriesunanimous_small_n_on_volatile_path, re-run with more samples or carry the flag into the report. - Reference the CSV, and check
csv.rowsagainsttotals.filesbefore claiming completeness. The CSV is the artifact in which "every file" literally exists; the JSON summary is not. Summarise in chat, cite the CSV path and its row count, and never silently drop 99% of the tree.
State that the tree was live (quiesced: false). Counts drift while a scan runs, and any orphan
count keyed on sessions carries a margin of error, because a session whose record vanished mid-run is
unknown, not dead.
If you fan this out across agents, keep an explicit cross-review stage run by a fresh-context reviewer that did not produce the findings. Parallelism buys coverage, not correctness, and a producer rarely catches its own error. Verify a peer's claim against your own evidence before adopting it, and record a disagreement nothing depends on as unresolved rather than settling it silently. Cross-review procedure: see reference/evidence-discipline.md.
Verifying an upstream claim
Any claim about what Claude Code itself does must come from the raw markdown endpoint. curl -sSL
https://code.claude.com/docs/en/claude-directory.md to a file, then read the file. A summarizing
fetch returns a small model's answer about the page, so absence from it is not evidence of
absence, and no destructive conclusion may rest on one.
Upstream-claim verification: see reference/evidence-discipline.md §6.
Gotchas
- A number in a filename is not a PID until proven otherwise. The most expensive error in this problem space, and the reason the liveness gate is code rather than advice.
enabledPlugins: falsedoes not mean disabled. Enablement spans several scopes, plugin hooks live in each plugin's own manifest, direct-path invocations fromsettings.jsonbypass the plugin system, and enablement is read at session start. This skill emitsrecent_writersas behavioural evidence and tell the user to run/claude-ops:plugins auditfor the verdict.backups/cannot be pruned meaningfully. It is a small rotating buffer that the product refills on its own. Any per-file finding about it is stale before it is written.- An empty directory may be deliberate. An empty
skills/can be an experiment's independent variable, not decay. Phase 1 exists for this. commands/,todos/,statsig/,logs/being absent is good news. It is positive evidence the sweep completed, including its remove-the-empty-directory step.- The "safe, no judgment required" tier is the one most in need of an independent check. A case-insensitive comparer collapses deny rules that differ only by case, so a "mechanically provable" dedupe can drop protections. This skill's deny matching is case-sensitive and tested; it encodes no dedupe or subsumption logic at all.