Lazy-Loading AGENTS.md and User-Only Skill Invocation

Second /doctor pass, one day after 2026-07-24 Claude Code Context Audit - Cutting Superpowers and 8 Plugins cut ~7.2k tokens. With the bloat gone the remaining findings are a thin tail: ~800 est tok/session across four groups. Install health, version currency (2.1.220, latest), permission posture (auto mode already default), and hooks were all clean again — PreToolUse:Bash median 41ms over 3,102 runs, zero timeouts.

Applied one group; declined three.

The insight worth keeping: user-only invocation is free deletion

Asked what disable-model-invocation: true would save on interactive-pr-review (0 model dispatches since install, 24k-char body). The answer reframes the whole keep-or-cut question:

~66 est tokens — exactly what deleting the skill would save. The resident cost is the listing entry (21-char name + 236-char description). The body never loads until invocation either way. So the flag is strictly better than removal: identical saving, and /review-pr keeps working.

OptionSaved/review-pr worksModel can auto-trigger
disable-model-invocation: true~66yesno
Remove the skill~66nono
Leave as-is0yesyes

Generalizes: for any skill whose value is real but whose routing value is zero, user-only is the dominant option. “Unused” was the wrong axis — the right question is whether the model needs to know it exists.

Caveat recorded honestly: the docs (claude-automation-recommender/references/skills-reference.md, plugin-dev/command-development) confirm the flag’s user-✓/model-✗ semantics but never state the listing entry is dropped. Couldn’t verify in-session — no enabled extension sets the flag to true, so there was no positive control to check my own listing against. The false case does have one (code-review sets it explicitly and does appear). Inference, not measurement. Verify with /context in a fresh session.

APFS makes deployed skill filenames sticky

Real gotcha, found by accident. ~/.claude/skills/interactive-pr-review/ deploys its entrypoint as lowercase skill.md — the only one of 20 skills like that — even though the repo source and the Nix store target are both SKILL.md.

hm switch did not fix it. HM rebuilt the symlink (new store hash, new timestamp) still pointing at …/SKILL.md, but the directory entry name stayed lowercase. On a case-insensitive filesystem the existing entry name wins when a file is recreated with different casing.

Why nothing caught it: validateSkillDir in common/lib.nix:63 exists precisely for this — its comment even says “Case-insensitive filesystems (macOS APFS) hide the problem locally but break on Linux deployments.” But it validates the source directory, which is correctly SKILL.md. It cannot see a stale deployment. A validator pointed at the input can’t catch drift in the output.

Consequence: on the Linux hosts the skill would silently fail to load. Fix is rm -rf the deployed dir (it holds nothing but that one store symlink) then hm switch. Left undone for now.

AGENTS.md: trimmed, then migrated

Root AGENTS.md 3,460 → 2,110 chars (~865 → ~528 est tok).

Cut as derivable: the Just Tasks block (just --list prints the same three recipes with the same descriptions) and Verification Commands (standard invocations, and its last line duplicated §5). Also dropped a stale (Use codebase_investigator) reference in the change protocol — that agent isn’t registered.

Migrated §5–§7 (HM option discovery, packages.nix patterns, Tridactyl remote loading) into a new nix/AGENTS.md + nix/CLAUDE.md stub, matching the nvim/ pattern. ~218 est tok now load only when working under nix/. Kept in root: the tier table, the change protocol, and §1’s “NEVER modify symlinked config files” — safety prohibitions never move to lazy loading.

Declined this round (still on the table)

4 dead skills (interactive-pr-review — since handled via the flag instead, python-configuration, python-observability, mermaid), 4 plugins (code-review, frontend-design, gopls-lsp, rust-analyzer-lsp), 2 MCP servers.

Signal quality notes for whenever this is revisited:

  • acl-anthology has never been called in any transcript, ever — a full sweep of all 774 transcripts, not just the 50-session window. mcp-image: 1 call all-time. arxiv is dormant, not dead (223 calls, last May).
  • commit-message looked like an easy cut (1 dispatch, last February) — but the commit skill, at 319 uses, invokes it by name. Removing it would have broken the single most-used skill. Lifetime counters don’t show dependency edges.
  • LSP plugins (gopls-lsp, rust-analyzer-lsp) contribute 0 resident tokens — removing them saves a process, not context. Their counters are the only signal (transcripts can’t attribute LSP activity), and tracking is recent.

Routing note

Everything user-scope here is generated by claude.nix / common/mcp.nix, so /doctor’s normal “edit ~/.claude/settings.json” advice is wrong for this setup — those files get clobbered by the next hm switch. Every proposal had to be re-routed to the checked-in Nix modules.

Related: Claude Code Skills - How the Model Sees Them, Claude Code Skills - Trigger and Dispatch