config-audit/commands/tokens.md
Kjell Tore Guttormsen 0b763f25c1 fix(commands): router dogfood — five seam defects, plus the placeholder class
Dogfooding `/config-audit` (the router) against the repo, fasit written before
any run (docs/router-fasit.local.md, untouched). Every claim below is measured
behaviour, not a reading of the source.

1. Bare `<target-path>` inside the step-3 fence is a shell REDIRECTION, not an
   argument. Measured in zsh: both CLIs failed before starting, no output file
   was written, and the echoed status was 1 — inside the band the router's own
   gate calls "continue normally". Quoting makes an unsubstituted placeholder
   reach argv, so it fails in the CLI where the exit code means something.
   Swept the whole class: 30 sites across 12 further command files, since a
   defect in one file is a class until the opposite is measured. New guard:
   command-placeholder-shell-safety.test.mjs.

2. The orchestrator's exit code was discarded. Two commands on one line share a
   single trailing `echo $?`, which reports only the last: measured, an
   orchestrator exit 3 echoed as posture's 0, so the "3 -> stop" gate could
   never fire. Both statuses are now captured and echoed.

3. "Running 12 configuration scanners" — the orchestrator registers 16. The new
   test binds the narrated count to the registry so the next scanner added
   cannot re-stale it silently.

4. The Area Breakdown table hardcoded 7 rows; posture emits 9 quality areas.
   Token Efficiency (a B on this repo) and Plugin Hygiene never reached the
   user. Rows added, and the row set is now asserted against lib/scoring.mjs.
   Label aligned: "MCP Servers" -> "MCP", as posture emits it.

5. Step 6 rendered "the headline line from the humanized stderr scorecard" and
   forbade deriving a replacement — while step 3 sent posture's stderr to
   /dev/null, as UX rule 2 requires, and the prose is absent from the JSON
   payload (measured). The slot could only be improvised. posture's stderr now
   goes to a file in the session dir, as commands/posture.md already did; the
   user still never sees raw scanner output.

Also: `grep -q -- "--raw"` matched any argument CONTAINING --raw (measured on
`--rawdog` and on a path with --raw in it) — anchored to whole arguments.
SCOPE_FLAGS renamed SCOPE_FLAG, since zsh does not word-split and the plural
invited the M-BUG-45 shape.

command-shell-state-shape.test.mjs only recognised line-initial assignments, so
it reported the idiomatic `node …; STATUS=$?` capture as never assigned. Widened
to assignments after a separator; verified it still fails on a real cross-block
reference before trusting it.

Suite 1477 -> 1483, frozen v5.0.0 snapshots untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDAwy1ZXRpZxht1wyCeSbF
2026-08-09 21:18:05 +02:00

8.4 KiB

name description argument-hint allowed-tools model
config-audit:tokens Show ranked token hotspots and prompt-cache pattern findings — what's costing the most per turn and how to reduce it [path] [--global] Read, Bash sonnet

Config-Audit: Token Hotspots

Show the configuration sources that contribute the most tokens per turn, ranked by estimated tokens, with prompt-cache-aware recommendations for reducing cache misses, schema bloat, and deep import chains.

Complementary to /config-audit whats-active:

  • whats-active = inventory view (what loads).
  • tokens = action view (what to trim and why).

UX Rules (MANDATORY — from .claude/rules/ux-rules.md)

  1. Never show raw JSON or stderr output. Always use --output-file + 2>/dev/null.
  2. Narrate before acting. Tell the user what you're about to do.
  3. Read, don't dump. Read the JSON file and render formatted tables.
  4. End with context-sensitive next steps.

Implementation

Step 1: Parse $ARGUMENTS

Split $ARGUMENTS into a path and flags. Path is the first non-flag argument. Default to . (current working directory). Recognized flags:

  • --global — also include the user-level ~/.claude/ cascade
  • --no-exclude-cache — include stale ~/.claude/plugins/cache versions in the ranking. By default they are excluded (cache-aware filtering, default ON): the cache holds superseded plugin versions that load on zero turns, and counting them used to crowd the top-10 with dead config. The active version of each plugin (per installed_plugins.json) is always kept — only stale versions are filtered. Use --no-exclude-cache to see the full on-disk walk.
  • --json — emit raw JSON instead of rendered tables (power-user mode; bypasses the humanizer for byte-stable v5.0.0 output)
  • --raw — pass-through to the scanner; produces v5.0.0 verbatim JSON (bypasses the humanizer). Use when piping into v5.0.0-baseline diff tooling.
  • --with-telemetry-recipe — include telemetry_recipe_path in the JSON output, pointing to knowledge/cache-telemetry-recipe.md. Use this when you want to verify a structural fix actually improved cache hit rate (manual jq recipe, opt-in)

Step 2: Run the CLI silently

Tell the user: "Analysing token hotspots for <path>..."

Default mode (no --json, no --raw) emits a humanized JSON envelope: each finding carries userImpactCategory, userActionLanguage, and relevanceContext in addition to the v5.0.0 fields. Pass --raw through verbatim if the user requested it.

RAW_FLAG=""
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
# Set each to the flag itself when the user asked for it, otherwise leave empty.
# A placeholder in square brackets does not start with a dash, so the CLI's arg
# loop would take it as the TARGET PATH instead of a flag.
GLOBAL_FLAG=""           # --global
CACHE_FLAG=""            # --no-exclude-cache
JSON_FLAG=""             # --json
TELEMETRY_FLAG=""        # --with-telemetry-recipe
node ${CLAUDE_PLUGIN_ROOT}/scanners/token-hotspots-cli.mjs "<path>" --output-file /tmp/config-audit-tokens.json $GLOBAL_FLAG $CACHE_FLAG $JSON_FLAG $TELEMETRY_FLAG $RAW_FLAG >/dev/null 2>/dev/null; echo $?

--json and --with-telemetry-recipe must be threaded here, not just documented: the CLI is what turns them on. --json/--raw make the payload byte-stable v5.0.0 (the humanizer is skipped, token-hotspots-cli.mjs:127), and --with-telemetry-recipe is what adds telemetry_recipe_path. >/dev/null is required because those two modes also print the payload to stdout even with --output-file set (token-hotspots-cli.mjs:137).

Exit code handling:

  • 0 → continue
  • 3 → tell user: "Couldn't analyse tokens. Check that the path exists and is a directory." Stop.

Step 3: If --json was requested, cat the file and stop

cat /tmp/config-audit-tokens.json

Do NOT render tables in JSON mode.

Step 4: Read JSON and render

Use the Read tool on /tmp/config-audit-tokens.json. Extract:

  • total_estimated_tokens — top-line number
  • hotspots[] — top 10 ranked sources; each carries a load pattern (loadPattern ∈ always / on-demand / external, plus survivesCompaction / derivationConfidence)
  • findings[] — prompt-cache pattern findings; each finding in default mode carries humanizer fields (userImpactCategory, userActionLanguage, relevanceContext) alongside the v5.0.0 fields
  • counts — severity breakdown

A hotspot's load pattern matters as much as its size: an always-loaded source (CLAUDE.md, MCP tool schemas) is paid on every turn, an on-demand one (skill body, path-scoped rule) only when invoked/matched, and an external one (hooks, harness-config files like settings.json/.mcp.json) costs no per-turn context tokens at all. A big always-loaded hotspot is the most worth trimming.

The stale plugin-cache finding is different from the rest. All other TOK findings are about per-turn token cost. The "Old plugin versions are sitting on disk" finding (category plugin-cache-hygiene, impact Dead config, --global only) is a pure disk-cleanup item with zero live-context impact — the listed versions are never loaded. Render it as housekeeping, not a token problem: don't conflate its disk bytes with the per-turn token numbers above it.

Render as markdown. Group findings by userImpactCategory (e.g., "Wasted tokens" vs "Configuration mistake") rather than re-deriving severity prose; lead each line with userActionLanguage ("Fix this now", "Fix soon", "Optional cleanup", etc.) so the urgency phrasing stays consistent with the rest of the toolchain. The humanizer already replaced jargon-heavy title/description/recommendation strings with plain-language equivalents — render them verbatim.

**Token hotspots for `<path>`** — ~{total_estimated_tokens} estimated tokens loaded per turn

### Top hotspots (ranked by estimated tokens)

| Rank | Source | Tokens | Load | Recommendations |
|------|--------|--------|------|-----------------|
| {rank} | `{source}` | ~{estimated_tokens} | {loadPattern} | {recommendations joined as `· ` bullets} |

_Load column: **always** (every turn) / **on-demand** (on invoke/match) / **external** (out-of-context). Append `°` when `derivationConfidence` is `inferred`._

### Findings, grouped by impact

{Group findings[] by their userImpactCategory. Within each group, sort by userActionLanguage urgency (Fix this now → Fix soon → Fix when convenient → Optional cleanup → FYI), then render:}

- **{userActionLanguage}** — {title}  ({id})
  - {description}
  - **Fix:** {recommendation}
  - _{relevanceContext}_ when not "affects-everyone" (mention the scope so the user knows whether a fix touches shared config or just their machine)

### Severity summary

| Severity | Count |
|----------|-------|
| critical | {counts.critical} |
| high | {counts.high} |
| medium | {counts.medium} |
| low | {counts.low} |
| info | {counts.info} |

_Estimates assume ~4 chars/token (Claude ballpark). Real token count varies ±20%._

Step 5: Cleanup and next steps

rm -f /tmp/config-audit-tokens.json
### What's next

- **`/config-audit whats-active`** — full inventory of what loads (plugins, skills, MCP, hooks)
- **`/config-audit posture`** — overall health scorecard (Token Efficiency is the 8th area)
- **`/config-audit fix`** — auto-fix deterministic issues (where applicable)
- See `knowledge/prompt-cache-patterns.md` for the full pattern catalogue (CA-TOK-001 … 003)
- **Verify cache hit rate after a fix:** rerun with `--with-telemetry-recipe` to surface the path to `knowledge/cache-telemetry-recipe.md` — a copy-paste `jq` recipe that reads cache hit rate from your session transcripts. Opt-in. The TOK scanner is structural; this recipe is the runtime escape hatch.

Scope and limits

  • Read-only. Inspects config files; never writes.
  • Single repo. Scans one path per invocation.
  • Structural only. Hotspots are deterministic byte→token estimates from disk; runtime cache hit-rate is out of scope.
  • Heuristic estimates. ~4 chars/token for markdown, ~3.5 for JSON. Real counts vary ±20%.

Error handling

Condition Action
Exit code 3 Tell user path is invalid, suggest checking path exists
JSON parse fails Tell user to re-run, mention as a bug to report
Empty hotspots Suggest adding a CLAUDE.md or running /config-audit feature-gap first