/tune-up — Claude Code Workflow Audit
Runs a concrete, step-by-step audit of the Claude Code setup for the current project. Reports findings with a uniform per-phase output, then presents a summary with proposed actions. Does NOT apply changes without approval.
How to execute
Parse $ARGUMENTS. Recognised flags:
/tune-up— full audit; always re-fetches upstream sources/tune-up --offline— skip every upstream probe (Phase 1's network leg, and the upstream half of Phases 3, 4, 5, 7). Local-only checks still run./tune-up --quick— Phase 1 (local-only) + Phase 2 + Phase 10 only.
Run phases in order. For each phase, follow the numbered steps exactly, then emit the output template. After Phase 10, present the summary and wait for user approval before running Phase 11.
Reading and searching. Prefer the dedicated tools — Read for files, Grep
for content search, Glob for listing paths — because they go through the
harness's permission and telemetry layers, while bash equivalents bypass both
and obscure what the audit actually consulted.
Prefer, not require: Grep and Glob are not offered to every session, and a
skill that insists on a tool the session does not have cannot run at all. Check
what you actually have before the first read. Where a dedicated tool is missing,
use the Bash equivalent (grep, find, sed -n) and say so once in the
Phase 1 output, so the reader knows this run's evidence came through a different
path. Do not silently substitute, and do not stall.
Steps below name operations rather than tools for this reason: "list */SKILL.md
under ~/.claude/skills/" is satisfiable either way; Glob(...) is not.
Bash is required regardless for shell-only operations: gh release list, the
state_io.sh / upstream_probe.sh helpers, the binary probe in Name
resolution, and atomic file moves.
Design principles
- Flat phase numbering. No nested sub-phases. Twelve peer phases.
- One subject per phase. Each phase audits exactly one concept.
- Uniform phase shape. Every phase has Subject / Sources / Checks / Output / Apply branches.
- Single state file. Per-project at
<project>/.claude/.tune-up-state.json. No XDG state, no shared cache, no TTL. - Canonical flag vocabulary (table below). Same flag means the same thing in every phase.
- Always re-fetch upstream. No
--refresh.--offlineskips upstream.
Canonical flag vocabulary
| Flag | Meaning |
|---|---|
[duplicate] |
Same thing defined in multiple places |
[redundant] |
Subsumed by a broader rule |
[unused] |
Defined locally/globally but never referenced by this project |
[stale-ref] |
Reference points at something that no longer exists |
[outdated] |
Local copy older than authoritative source |
[upgrade-available] |
New version available in marketplace/CLI release |
[adopt] |
Codebase pattern suggests adopting an automation (hook, subagent, skill, plugin, MCP) — sole source: Phase 10 via /claude-code-setup:claude-automation-recommender |
[new-feature] |
Upstream added something since last tune-up |
[withdrawn] |
Was available to this session last run, is not now |
[phantom-tool] |
A name in config resolves to no tool anywhere — delete the reference |
[gated-tool] |
Names a tool that exists in the CLI but is withheld from this session — keep the reference, scope it |
[mismatch] |
Local config inconsistent with stated policy |
[missing] |
Something expected from project type/tech stack is absent |
[oversized] |
Exceeds size threshold |
[quality:A] … [quality:F] |
CLAUDE.md quality grade from claude-md-improver (Phase 9) |
[budget-warning] / [budget-critical] |
Context budget thresholds |
Name resolution (shared by Phases 2, 3, 4, 7, 8, 9)
Several phases check whether a name written in config actually refers to something the harness will resolve. They all use this one ladder, so a name gets the same verdict wherever it appears:
name → Phase 1's live block (tools_available / agents_available / skills_available)
├─ found → OK
└─ not found → Bash: grep -ac '"<Name>"' "$(readlink -f ~/.local/bin/claude)"
├─ hits > 0 → [gated-tool]
└─ hits = 0 → [phantom-tool]
The second step matters because "unavailable" has two causes with opposite fixes. A name can be fictional — it never existed, and the reference should be deleted. Or it can be a real tool the CLI ships but withholds from this session, via a remote feature flag or a model-version threshold. Deleting a rule for a tool that returns next month is the wrong repair, and the two cases are indistinguishable from the live list alone; only the binary tells them apart. The grep runs solely on names that already failed to resolve, so its cost stays proportional to the problem rather than to the size of the config.
Match the quoted token, not the bare name. Tool names appear in the binary's
registry as "Name", and an unquoted search matches every substring: LS
returns 3078 raw hits against 1 quoted. Short names are the risk — a genuinely
fictional two- or three-letter name will collide with ordinary text and be
reported as gated, which is the precise error the two flags exist to prevent,
and it fails in the direction that tells the user to keep a dead reference.
Report a [gated-tool] with the advice to scope the reference ("when
available") rather than remove it, and say which gate is plausible if the
surrounding evidence shows one.
State file: <project>/.claude/.tune-up-state.json
Single JSON document, gitignored by default. Schema:
{
"schema_version": 2,
"last_run_iso": "2026-05-02T19:14:00Z",
"local": {
"claude_code_cli_version": "v2.3.1",
"agents_present": ["..."],
"skills_present": ["..."],
"plugins_present": ["..."],
"mcp_servers": ["..."],
"settings_hooks_fingerprint": "sha256-of-merged-hooks",
"installed_plugins_fingerprint": "sha256-of-installed_plugins.json"
},
"live": {
"observed_with_model": "claude-opus-5",
"tools_available": ["..."],
"agents_available": ["..."],
"skills_available": ["..."],
"hook_events_available": ["..."]
},
"upstream_at_last_run": {
"claude_code_latest_release": "v2.3.1",
"release_notes_max_date": "2026-04-30",
"marketplace_plugins": ["..."],
"announced_features": ["..."]
},
"acknowledged_upstream_features": ["agent:data-engineer", "..."],
"pending_ansible_mirror": [
{ "applied_at": "...", "target": "...", "diff_sha256": "..." }
]
}
Read once at start of Phase 1; rewritten atomically at end of Phase 11. Use
the state_io.sh helper (in ${CLAUDE_SKILL_DIR}/scripts/state_io.sh) to
avoid corrupt-on-crash.
Phase 1 — Drift Setup
Subject. Load the snapshot, recompute current local fingerprints, fetch
upstream state (unless --offline), print the "since last tune-up" banner.
Sources.
<project>/.claude/.tune-up-state.json(or initialise if absent).- Project tree (for local fingerprints).
- The running session itself — the in-context tool, agent and skill
listings, plus
ToolSearchto resolve deferred tools by name. This is the authority on what exists; the release notes below are only the authority on what is new. (Why: a tool can ship in the binary and still be withheld from a session by a remote feature flag or a model-version threshold, and no changelog bullet is ever written for that. Existence derived from release notes cannot see it, so the audit would keep endorsing config that names something the session does not have.) gh release list --repo anthropics/claude-code -L 5 --json tagName,publishedAtclaude-code-guidesubagent (primary release-notes fetcher — handles docs URL changes via WebSearch, then WebFetch). RawWebFetchonhttps://docs.claude.com/en/docs/claude-code/release-notesis the fallback.~/.claude/plugins/marketplaces/*/(disk only)~/.claude/plugins/installed_plugins.json~/.claude/plugins/known_marketplaces.json
Steps.
Run
Bashon${CLAUDE_SKILL_DIR}/scripts/state_io.sh readfrom$CLAUDE_PROJECT_DIR. The helper prints the current snapshot JSON to stdout, or an empty default schema if the file is missing. Parse the output. Ifschema_versionis greater than2, emit[stale-ref]"snapshot from a future schema; aborting" and stop the run. Aschema_versionof1predates theliveblock: accept it, treat the missing block as a first-run baseline for availability, and emit no[withdrawn]this run — there is nothing to compare against yet.Recompute the
localblock:claude_code_cli_version:Bash("claude --version 2>/dev/null | head -1")agents_present: list*.mdin.claude/agents/, strip.mdskills_present: list*/SKILL.mdin.claude/skills/plus~/.claude/skills/, strip/SKILL.mdplugins_present: parse~/.claude/plugins/installed_plugins.json, extract everyplugins.*keymcp_servers: parse<project>/.mcp.jsonmcpServerskeys plus anything in~/.claude/settings.json:mcpServerssettings_hooks_fingerprint: concatenate the mergedhooksconfig from all four settings files (in priority order) andsha256suminstalled_plugins_fingerprint:sha256sum ~/.claude/plugins/installed_plugins.json
Then recompute the
liveblock — what this session actually has, which is not the same question as what is installed on disk:tools_available: every tool name offered to this session — the base toolset plus every deferred tool named in context. Resolve anything you are unsure of withToolSearch("select:<Name>")rather than assuming.observed_with_model: the model id powering this session. Record it every time. Availability is model-dependent, so a tools list stored without the model that observed it will read as a withdrawal the next time the user runs on a different tier.agents_available/skills_available: from the in-context listings, unioned with the on-diskagents_present/skills_presentabove.hook_events_available: the hook event names the harness accepts.
Compute the
local_driftdiff against the snapshot'slocalblock: added/removed agents, added/removed skills, added/removed plugins, hook fingerprint changed yes/no. Hold this in memory for the banner.If
--offline: skip steps 5-9. Use snapshot'supstream_at_last_runas both baseline AND current (so upstream-drift checks emit nothing).Issue steps 5a-5b in parallel — two independent probes; the model should call both tools in a single message rather than serialising:
5a.
Bash-run${CLAUDE_SKILL_DIR}/scripts/upstream_probe.sh. Returns JSON withgh_releases(top 5 tags + dates),marketplace_plugins(sortedplugin@marketplaceIDs),installed_plugins(rich metadata: scope, enabled, installPath, installedAt, lastUpdated, mcpServers),known_marketplaces. The helper itself runsghand the plugin enumeration in parallel internally.5b. Spawn a
claude-code-guidesubagent (it has WebSearch + WebFetch and is purpose-built for navigating Anthropic docs URL changes). Prompt: "Find the current Claude Code release notes page (the docs URL has redirected before — use WebSearch withsite:docs.anthropic.com OR site:docs.claude.com claude-code release-notesto discover the live URL, then WebFetch it). List every bullet added since{snapshot.release_notes_max_date}. For each, classify as: built-in agent, built-in tool, hook event, settings field, plugin marketplace addition, other. Output as JSON:{added_bullets: [{date, category, name, summary}]}. Also extract the canonical lists of built-in agents and hook events from the docs in case the release-notes page is sparse."Safety net. If the agent returns empty/error, fall back to the raw
WebFetch https://docs.claude.com/en/docs/claude-code/release-noteswith the same prompt — older URL may still resolve. Merge any results. Theclaude-code-guidearm is the primary path because it's strictly more capable; the rawWebFetchis the fallback, not the other way around.
Build the
upstream_currentblock from step 5's results:claude_code_latest_release= newesttagNamefromgh_releasesrelease_notes_max_date= maxdateacrossadded_bullets(or unchanged if zero new bullets)marketplace_plugins= from upstream probeannounced_features=added_bulletsgrouped by category. These feed[new-feature]only: they record what upstream announced, not what this session has. Do NOT merge with snapshot values (the diff in step 7 vsupstream_at_last_runalready handles baseline + acknowledgement filtering).- Nothing here describes what the session has.
tools_available,agents_availableandhook_events_availablewere already recorded from the live probe in step 2 and are never derived fromadded_bullets. Something can be announced and unavailable, or available and never announced; only the live probe tells you which you are looking at.
Compute
upstream_driftdiff:upstream_currentminusupstream_at_last_run, minus already-acknowledged_upstream_features. Then computeavailability_driftfrom this run'sliveblock against the snapshot's:- available last run, absent now →
[withdrawn] - available now, absent last run →
[new-feature]
Model guard. If the snapshot's
observed_with_modeldiffers from the current model, report every availability delta as informational and suppress[withdrawn]entirely. The set is model-dependent, so a tier change already explains the difference; flagging it as a withdrawal would be wrong on the facts and would train the user to ignore the flag.- available last run, absent now →
Emit the output:
Phase 1 — Drift Setup
Snapshot: .claude/.tune-up-state.json (last run: {iso}, {N days} ago)
OR "first run, establishing baseline"
Local CLI: {version}
Upstream CLI: {version} {[upgrade-available] | OK}
Live probe: {N} tools, {M} agents, {P} skills available to {model}
Release notes: {N} new bullets since {date}
Marketplace: {N} marketplaces fresh on disk
Local drift since last run:
{one line per [+]/[-]/[Δ] item, or "no changes"}
Availability drift:
{one line per [withdrawn]/[new-feature] item, or "none",
or "model changed {old} → {new} — deltas informational this run"}
Pending Ansible mirrors: {count, only if > 0 — list each target}
Apply branches. None. Phase 11 writes the snapshot.
Phase 2 — Permissions
Subject. Audit permissions.allow arrays across the four settings files.
Sources.
~/.claude/settings.json(Ansible-managed, editable with mirror policy)~/.claude/settings.local.json(global local)<project>/.claude/settings.json(project shared)<project>/.claude/settings.local.json(project local)
Priority order (highest first): global shared > global local > project shared > project local.
Steps.
- Read all four settings files via
Read(skip any that don't exist). - From each, extract every entry in the
permissions.allowarray. - Apply checks:
[oversized]— entry > 100 chars AND no*wildcard.[redundant]— entry covered by a broader wildcard at same or higher priority. Match: strip the suffix after the last:or(and check if a higher-priority entry has the same prefix.[duplicate]— exact match across files.[mismatch](consolidation cluster) — three or more entries in the same file share a common prefix; emit one flag per cluster.[mismatch](promotion candidate) — entry sits in~/.claude/settings.local.jsonor<project>/.claude/settings.local.jsonbut its scope looks machine-wide (no project-specific path, no~/Projects/<specific>, no host-specific binary). Suggest promoting up to the shared file in the same scope tier.[mismatch](demotion candidate) — entry in~/.claude/settings.jsonor<project>/.claude/settings.jsonlooks project-specific or host-specific (contains a~/Projects/<name>path or a binary unique to one host). Suggest demoting down.[mismatch](dead path rule) — entry namesWrite,NotebookEdit,MultiEditorGloband carries a path/glob argument rather than the:*prefix syntax. File permission checks consult onlyEdit(path)andRead(path)rules, so these never match anything and the permission the user thinks they granted is not granted. Emit the rewrite:Write/NotebookEdit/MultiEdit→Edit,Glob→Read. Check whether the rewritten rule already exists in the same file — if it does, the entry is dead weight rather than a missing grant, so the fix is to delete it and the flag should say so. Getting this backwards silently duplicates a rule that was already working.[phantom-tool]/[gated-tool]— entry names a tool that does not resolve (see Name resolution). The rule can never match, because nothing will ever request that tool.
- Emit:
Phase 2 — Permissions
~/.claude/settings.json: {N} entries
~/.claude/settings.local.json: {N} entries
.claude/settings.json: {N} entries
.claude/settings.local.json: {N} entries
Issues:
- [redundant] .claude/settings.local.json: "Bash(git branch:*)" covered by global "Bash(git:*)"
- [oversized] .claude/settings.json: "Bash(psql -h ...)" (128 chars, no wildcard)
- [duplicate] "Bash(npm:*)" appears in both global local and project shared
- [mismatch] 3 entries in .claude/settings.json share prefix "Bash(poetry run pytest" — consolidate?
- [mismatch] "Bash(gh*)" in settings.local.json looks machine-wide → promote to ~/.claude/settings.json
- [mismatch] ~/.claude/settings.json: "Write(//tmp/**)" is never consulted → "Edit(//tmp/**)" (already present — delete this entry)
- [phantom-tool] .claude/settings.json: "Snip(*)" — no such tool resolves
(or "No issues found." if clean)
Apply branches. Edit permissions.allow in the appropriate file.
Edits to ~/.claude/settings.json follow the Ansible-mirror policy
in Phase 11.
Phase 3 — Agents
Subject. Audit project agents, global agents, plugin-provided agents, and the upstream live built-in list.
Sources.
<project>/.claude/agents/~/.claude/agents/~/.claude/plugins/cache/*/*/agents/(plugin-provided)- Snapshot's
live.agents_available(previous run) - Phase 1's
live.agents_available(this run) — the authority on what exists
Steps.
List project agents:
*.mdin.claude/agents/. If the directory is missing, report and continue.List global agents:
*.mdin~/.claude/agents/.List plugin agents:
*/agents/*.mdunder~/.claude/plugins/cache/.For each project agent, read the first 30 lines.
Apply checks:
[mismatch]— project agent filename matches a name in this run'slive.agents_availableAND the file has no project-specific terms (project name frompyproject.toml/package.json, framework class names, domain-specific tools). This shadows a built-in without adding value.[stale-ref]— agent name (lowercase-hyphenated) referenced inCLAUDE.md,~/.claude/CLAUDE.md, or any.claude/skills/*/SKILL.md, but no matching file exists in any of the three locations (project, global, plugin).[missing]— project tech stack expects a specialist that's absent:.ipynbfiles exist → expect a notebook agentDockerfile/docker-compose.yml→ expect a docker agentpyproject.tomlmentionspandas/numpy→ expect a data agent
[unused]— globally-installed or plugin-provided agent never referenced in this project's tree (search for the bare name across.claude/,CLAUDE.md, top-level docs).[phantom-tool]/[gated-tool]— an agent definition'stools:frontmatter names a tool that does not resolve (see Name resolution). The agent still loads, so nothing errors; it just quietly has less reach than its definition claims.[new-feature]— name in this run'slive.agents_availablebut not in the snapshot's AND not inacknowledged_upstream_features.
Adoption gaps (codebase patterns suggest installing a new agent) are no longer audited here — Phase 10 owns that dimension via
/claude-code-setup:claude-automation-recommenderand emits[adopt]flags in the consolidated summary.Emit:
Phase 3 — Agents
Project agents: {N} ({comma-separated names})
Global unused: {names not referenced in this project}
Plugin agents: {N} provided by installed plugins
Issues:
- [mismatch] docker-expert.md — no project-specific content, shadows built-in
- [stale-ref] CLAUDE.md mentions "numpy-expert" — file does not exist
- [missing] project has 12 .ipynb files but no notebook-specialist agent
- [new-feature] new built-in agent: data-engineer (release notes 2026-04-22)
(or "No issues found." if clean)
Apply branches. Delete agent file; edit CLAUDE.md to fix stale ref;
add new-feature to acknowledged_upstream_features. Adoption candidates
appear only in Phase 10 ([adopt] flags from automation-recommender).
Phase 4 — Skills
Subject. Audit project + global skills, with the in-context skill listing and on-disk copies as the source of truth.
Sources.
<project>/.claude/skills/*/SKILL.md~/.claude/skills/*/SKILL.md~/.claude/plugins/cache/*/*/skills/*/SKILL.md(plugin-provided)- Phase 1's
live.skills_available - Git-tracked copies of the same skills elsewhere on disk (see
[outdated])
Steps.
List all skills across the three locations above.
For each project + global skill, read the full SKILL.md.
Apply checks:
[mismatch]— SKILL.md containsAllowed CommandsorBash(...)permission patterns (skills shouldn't carry permissions; that'ssettings.json's job).[mismatch]— SKILL.md contains "Shell Command Rules" or similar directive sections (those belong in CLAUDE.md).[stale-ref]— references an agent name (lowercase-hyphenated, no/prefix) that doesn't exist in any agent location.[stale-ref]— references adocs/path that doesn't exist (verify withBash("test -e <path>")).[stale-ref]— references${CLAUDE_SKILL_DIR}/scripts/<file>where the file doesn't exist relative to the skill's own directory.[phantom-tool]/[gated-tool]— SKILL.md names a tool that does not resolve (see Name resolution), whether in prose, in a Sources list, or inallowed-toolsfrontmatter. A skill whose instructions call a tool the session does not have fails partway through, at the point of use, with no warning beforehand.[outdated]— a skill installed under~/.claude/skills/has a git-tracked counterpart on disk that differs. Discover counterparts withBash("find ~/Projects -maxdepth 4 -path '*/skills/*/SKILL.md' 2>/dev/null"), mirroring the Ansible-repo discovery in Phase 11, and compare by content. Report the direction and size of the divergence — an installed copy ahead of its repo is unbacked work that dies with the disk, while one behind its repo is running stale. Both are worth knowing; neither is automatically the error.[unused]— globally-installed skill never referenced from this project tree.
Adoption gaps (skills the project should install) are no longer audited here — Phase 10 owns that dimension via
/claude-code-setup:claude-automation-recommenderand emits[adopt]flags in the consolidated summary.Emit:
Phase 4 — Skills
Project skills: {N}
Global skills: {M}
Plugin skills: {P}
Issues:
- [mismatch] /pre-commit-check carries hardcoded "Bash(pytest...)"
- [stale-ref] /post-push-check references docs/ci.md (does not exist)
- [outdated] /tune-up — installed 1171 lines, git copy 245 lines (installed is ahead, unbacked)
- [unused] /frontend-design — no UI work in this project
(or "No issues found." if clean)
Apply branches. Edit SKILL.md inline; print update command for
[outdated]; offer to disable [unused] skills via project settings.
Adoption candidates appear only in Phase 10 ([adopt] flags from
automation-recommender).
For [mismatch] flags that involve rewriting a skill SKILL.md (e.g. a
skill carries hardcoded permissions and needs to be split), prefer
delegating the rewrite to /skill-creator:skill-creator in Phase 11
rather than editing inline — it enforces the canonical SKILL.md
conventions and can run trigger-eval on the description.
Phase 5 — Plugins
Subject. Audit installed plugins for updates, channel mismatches, and project-level usage. Disk-only — no network.
Sources.
- Phase 1's
upstream_probe.shoutput, specifically:installed_plugins[]— rich metadata per installed plugin (id,version,scope,enabled,installPath,installedAt,lastUpdated,mcpServers,projectPathfor local-scope)marketplace_plugins[]— sorted<name>@<marketplace>IDs of every plugin available across configured marketplacesknown_marketplaces[]— marketplace metadata (name,source,lastUpdated)
- Snapshot's
upstream_at_last_run.marketplace_plugins(diff baseline) claude plugin list --available --jsonis the canonical source. The shell helper already invokes it (with on-disk fallback ifclaudeisn't on$PATH); Phase 5 reads from that result, never directly frominstalled_plugins.jsonormarketplaces/*/.claude-plugin/marketplace.json.
Steps.
- From
installed_plugins[], group byscope(uservslocal). Note any disabled (enabled: false) entries — informational, not flagged unless they're also stale. - For each entry in
known_marketplaces[], flag[stale-ref]"marketplace clone is more than 7 days stale" ifnow - lastUpdated > 7d(the harness should sync more often). - Cross-reference: the set of marketplaces that any installed plugin
came from must be a subset of
known_marketplaces[].name. Any mismatch is[stale-ref]"installed plugin from unknown marketplace". - Apply checks:
[upgrade-available]— installed plugin'sversionis older than the marketplace manifest's latest.[mismatch](channel) — plugin'schannelfield, when present (v3+ schema), differs from marketplace's recommended channel.[unused]— installed plugin's namespace (e.g.playwright@) never appears in this project's tree (search across.claude/,CLAUDE.md, top-level docs).[new-feature]— plugin inupstream_current.marketplace_pluginsbut not inupstream_at_last_run.marketplace_pluginsand not inacknowledged_upstream_featuresasplugin:<id>.
- Emit:
Phase 5 — Plugins
Installed: {N}
Referenced in proj: {M}
Marketplaces: {marketplace_id (last sync: ...)}
Issues:
- [upgrade-available] code-simplifier@claude-plugins-official 1.0.0 → 1.2.0
- [unused] playwright@claude-plugins-official (installed 2026-02-23, no project ref)
- [new-feature] marketplace added: notebook-tools@claude-plugins-official
- [stale-ref] marketplace claude-hud last synced 14 days ago
(or "No issues found." if clean)
Apply branches. Print claude plugin update <id>; print
claude plugin remove <id> (only on [unused], only after explicit
confirm); offer project-level disable list edit; add new marketplace
entries to acknowledged_upstream_features as plugin:<id>.
Phase 6 — MCP Servers
Subject. Audit MCP servers for redundancy, heavy startup, and policy mismatches. (Pure intra-project; no upstream dimension.)
Sources.
<project>/.mcp.json~/.claude/settings.json:mcpServers<project>/.claude/settings.json:enabledMcpjsonServers / disabledMcpjsonServers
Steps.
- Read
<project>/.mcp.jsonand themcpServersblock of any settings file that defines one. - Apply checks:
[redundant]—filesystemserver (Read/Write/Glob/Grep are built-in) orgithubserver (useghCLI; the MCP fork has fewer tools and slower startup).[mismatch](heavy startup) —commandusespodman run,docker run, ornpx.[oversized]—toolTimeout> 120 seconds.[mismatch](over-enabled) —enableAllProjectMcpServers: truein any settings file. Prefer selective enablement.
- Count total registered tools. If > 20, mention as informational "namespace pressure" note (not a flag).
- Emit:
Phase 6 — MCP Servers
Configured: {N}
Tool namespace: ~{N} tools (target: <20)
Issues:
- [redundant] filesystem — Read/Write/Glob/Grep are built-in
- [redundant] github — `gh` CLI covers this
- [mismatch] ssh-dispatch uses `podman run` (heavy startup)
- [oversized] alpaca: toolTimeout=300s
(or "No issues found." if clean)
Apply branches. Edit .mcp.json to remove the server; reduce timeout;
flip enableAllProjectMcpServers to selective enablement.
Phase 7 — Hooks & Environment Activation
Subject. Audit hooks config for env-activation completeness, hook-event
schema drift, and policy mismatches.
Sources.
- All four settings files (
hooksblock) - Snapshot's
live.hook_events_available(previous run) - Phase 1's
live.hook_events_available(this run) — the authority - Project-type detection:
pyproject.toml,environment.yml,pom.xml,build.gradle,build.gradle.kts
Precondition. CLAUDE_ENV_FILE must be set in the Claude Code process
environment. Recommended location: ~/.claude/settings.local.json
(machine-local, Ansible-safe):
{ "env": { "CLAUDE_ENV_FILE": "/tmp/claude-env-<user>.sh" } }
If absent, env-activation hooks silently no-op because $CLAUDE_ENV_FILE
expands to empty.
Steps.
- Check
~/.claude/settings.local.jsonand~/.claude/settings.jsonforenv.CLAUDE_ENV_FILE. If neither defines it, flag[mismatch]("CLAUDE_ENV_FILE unset; hooks will silently no-op"). - Detect project type:
pyproject.tomlcontaining[tool.poetry]→ Poetryenvironment.ymlormeta.yaml→ Condapom.xml,build.gradle,build.gradle.kts→ Java (Maven/Gradle)- None → report "None detected" and skip steps 3-5.
- Read
<project>/.claude/settings.jsonand<project>/.claude/settings.local.json. Inhooks.SessionStart[*].hooks[*].command, search for any string containing$CLAUDE_ENV_FILE(literal or"$CLAUDE_ENV_FILE"). - Apply checks:
[missing]— typed project but no SessionStart hook writing to$CLAUDE_ENV_FILE. Apply branch installs the project-type template (see Apply branches below).[mismatch]— env-activation hook lacksif [ -n "$VENV" ](or equivalent guard for the project type).[mismatch]— multiple hooks write to$CLAUDE_ENV_FILEand mix>(truncate) with>>(append). Only the first should truncate; subsequent ones should append. Mixed redirects in arbitrary order produce nondeterministic env content.
- Apply upstream checks:
[new-feature]— this run'slive.hook_events_availablecontains a name not in the snapshot's and not inacknowledged_upstream_featuresashook:<name>.[stale-ref]— settings register a hook under an event name that is not inlive.hook_events_available. This is silent in the worst way: the hook is well-formed, the settings file parses, and it simply never fires.[phantom-tool]/[gated-tool]— amatcherinPreToolUse,PostToolUseorPostToolUseFailurenames a tool that does not resolve (see Name resolution). Same silence: a matcher on a tool the harness never emits is indistinguishable from a hook that simply has not triggered yet.
- Emit:
Phase 7 — Hooks & Environment Activation
CLAUDE_ENV_FILE: {set in <file> | UNSET}
Project type: {Poetry | Conda | Java | None detected}
Env hook present: {yes (guarded, > redirect) | MISSING | unguarded}
Issues:
- [mismatch] CLAUDE_ENV_FILE unset; hooks will silently no-op
- [missing] Poetry project lacks venv-activation SessionStart hook
- [mismatch] hook command does not check if env exists before writing PATH
- [new-feature] upstream added hook event: SubagentStop
- [stale-ref] settings register a hook on "PreCommit" — not an accepted event name
(or "No issues found." if clean)
Apply branches.
For [missing], install the project-type SessionStart template.
Append a new entry to hooks.SessionStart in
<project>/.claude/settings.json (create the file with a standard
skeleton if absent). Do NOT add a matcher to the new entry — it must
fire on every session start, not just compact/resume. Preserve any
existing entries. Templates use > (truncate):
- Poetry:
VENV="$(cd "$CLAUDE_PROJECT_DIR" && poetry env info --path 2>/dev/null)"; \ if [ -n "$VENV" ] && [ -d "$VENV/bin" ]; then \ printf 'PATH=%s:%s\nVIRTUAL_ENV=%s\n' "$VENV/bin" "$PATH" "$VENV" > "$CLAUDE_ENV_FILE"; \ fi - Conda (read
name:fromenvironment.yml):CP="$(conda env list | awk -v n=<name> '$1==n {print $NF}')"; \ if [ -n "$CP" ] && [ -d "$CP/bin" ]; then \ printf 'PATH=%s/bin:%s\nCONDA_PREFIX=%s\n' "$CP" "$PATH" "$CP" > "$CLAUDE_ENV_FILE"; \ fi - Java (resolve via
update-alternatives --query javacon Linux or/usr/libexec/java_home -v <ver>on macOS):JH="$(...resolver...)"; \ if [ -n "$JH" ] && [ -d "$JH/bin" ]; then \ printf 'JAVA_HOME=%s\nPATH=%s/bin:%s\n' "$JH" "$JH" "$PATH" > "$CLAUDE_ENV_FILE"; \ fi
After installing, remind the user to restart Claude Code so the hook
fires; the LSP MCP server picks up the new $PATH only at session start.
For [mismatch] (CLAUDE_ENV_FILE unset), append
{ "env": { "CLAUDE_ENV_FILE": "/tmp/claude-env-${USER}.sh" } } to
~/.claude/settings.local.json.
For [mismatch] (unguarded hook or mixed redirect), edit the hook
command in place.
For [new-feature], add hook:<name> to
acknowledged_upstream_features.
Phase 8 — Rules & Context Budget
Subject. Audit .claude/rules/ for size, scope, duplication, and total
auto-loaded context budget.
Sources.
<project>/.claude/rules/**/*.md<project>/CLAUDE.md~/.claude/CLAUDE.md- Memory files (
MEMORY.mdand any files it links to) - Phase 1's
liveblock, via Name resolution
Steps.
List rule files:
**/*.mdunder.claude/rules/. If none, report "No rules directory" and skip rule-specific checks (still run the budget computation).For each rule file, measure size in bytes.
Apply checks:
[oversized]— file > 3 KB. Rules should be concise directives, not documentation. Tables, full code blocks, multi-paragraph prose belong indocs/, not rules.[mismatch](broad scope) — rule scoped**/*.py(or similar wide glob) but content is specific to a subpath (e.g. only relevant toframework/graph.py).[duplicate]— rule's first heading or description matches a CLAUDE.md section heading. Auto-loaded rules and CLAUDE.md duplication wastes context.[phantom-tool]/[gated-tool]— the rule names a tool that does not resolve (see Name resolution). Use the same extraction Phase 9 step 2 performs, over each rule file. Rules load automatically on every turn exactly like CLAUDE.md, so a phantom name here has identical blast radius and is worth the same flag.
Compute the context budget. Count only what the harness loads on every turn, because that is the cost the thresholds are calibrated against:
- project CLAUDE.md + global CLAUDE.md, plus anything they
@-import (an imported file is auto-loaded exactly like its importer, so it counts) - every file under
.claude/rules/ MEMORY.md— the index only
Individual memory files are not in this total. They load when a memory is recalled, not every turn, so counting the pool inflates the figure by however much history the user has accumulated and reports
[budget-critical]on a setup that is nowhere near it. A flag that fires on healthy config is worse than no flag: it trains the reader to skip the line.[budget-warning]if the per-turn total > 50 KB.[budget-critical]if the per-turn total > 100 KB.
Report the recall-time memory pool on its own line as context, unflagged. It is a real cost, just not a per-turn one, and its size is the user's call.
- project CLAUDE.md + global CLAUDE.md, plus anything they
Emit:
Phase 8 — Rules & Context Budget
Rules: {N} files, {total_kb} KB total
Budget breakdown:
CLAUDE.md (project + global + imports): {kb} KB
Rules (.claude/rules/): {kb} KB
Memory index (MEMORY.md): {kb} KB
Total auto-loaded per turn: {kb} KB [{OK | budget-warning | budget-critical}]
Memory pool (loads on recall, not counted above): {kb} KB across {N} files
Issues:
- [oversized] rules/code-style/patterns.md — 22 KB (target: <3 KB)
- [mismatch] rules/architecture/graph-pipeline.md scoped **/*.py but only relevant to framework/graph.py
- [duplicate] rules/code-style/testing.md duplicates CLAUDE.md "Testing Standards" section
- [gated-tool] rules/workflow/planning.md instructs "track steps via `TaskCreate`" — exists in the CLI, withheld from this session
Recommended trimming:
For each [oversized] file: move examples/tables to docs/{path}, keep only
the directive (what to do) in the rule file.
(or "No issues found." if clean)
Apply branches. Split / shrink / scope-narrow rule files; remove duplicated sections from CLAUDE.md.
Phase 9 — CLAUDE.md, Coordinators & Notification Sinks
Subject. Four related cross-cutting checks merged into one phase:
(1) CLAUDE.md cross-references and contradictions; (2) coordinator-agent
adoption gaps; (3) notification-sink coverage for long-running work;
(4) CLAUDE.md quality grade via /claude-md-management:claude-md-improver.
Sources.
<project>/CLAUDE.md~/.claude/CLAUDE.md- Phase 1's
liveblock, via Name resolution <project>/.claude/agents/*-coordinator.mdand~/.claude/agents/*-coordinator.md- All settings hooks (
Stop,SubagentStop,Notification) <project>/.mcp.jsonfor sink-style servers (PushNotification,RemoteTrigger, slack/discord/teams MCPs)- Project skills/scripts grep for
run_in_backgroundpatterns /claude-md-management:claude-md-improver(report-only, for the quality grade)
Steps.
- Read project + global CLAUDE.md.
- Extract every backtick-quoted name that looks like an agent, a skill, or a
tool — three patterns:
- lowercase-hyphenated (
`python-expert`) → agent - slash-prefixed (
`/run-tests`) → skill PascalCaseormcp__*(`` `TaskCrea
- lowercase-hyphenated (
…(truncated)