ZB Field Notes

73 skills tried, three still on disk

73 skills tried, three still on disk

Read the ledger, not your memory

Claude Code quietly keeps a usage ledger in ~/.claude.json. Under skillUsage there is one entry per skill you have ever invoked — usageCount, lastUsedAt — and it survives uninstalling the skill. It is a graveyard and a leaderboard in the same object, and I had never opened it.

I opened it because a /insights run told me a flattering story about how I work, and I wanted to check that story against what is actually on disk. Six months of accumulated config, measured instead of remembered:

MetricValue
First session2026-01-24
Sessions started511
Project directories used in123
Distinct skills invoked at least once73
Total skill invocations309
Skills invoked exactly once31
Skills invoked ten or more times4
Skills currently in ~/.claude/skills3
Hooks configured0

Four names — ship (41), blog-article (39), code-review (24), init (18) — are 122 of the 309 invocations. Thirty-nine percent of everything, from four things. Meanwhile 31 skills were invoked exactly once and never again.

What actually survived

Three user-scoped skills are on disk today: blog-article (publishes this post), pg-init-sql (wires Postgres init DDL into a compose project), and to-speech (my own cloned voice, running locally on CPU). That is it.

My single most-used automation is not a skill at all. /ship is a project slash command — .claude/commands/ship.md, committed to the repo it ships. /dev sits next to it and boots the two dev servers. They are versioned with the code they operate on, which turns out to matter.

The survivors all encode facts, not technique

This is the pattern, and it took reading the graveyard to see it. Not one of the things I kept teaches the model how to do something. Every one of them is a container for local facts the model cannot possibly derive:

  • ship.md — never commit to master; open the tracking issue before branching so the PR can close it; this repo merges with --merge, not squash; and CD ships only the image, so a Traefik label or volume change needs a manual step on the box.
  • dev.md — backend on 8080, Vite on 5173, Postgres on host port 5433; export a throwaway publish token for the run or the blog seed cannot authenticate; and if the local blog already has posts, leave it alone.
  • pg-init-sql/docker-entrypoint-initdb.d runs only when the data directory is empty; files execute in alphabetical order, so 10- sorts before 2-; and if Hibernate's ddl-auto is update, your DDL and the ORM will fight over the same tables.
  • blog-article — the exact HTML allowlist my server keeps, and which tag routes a post into which topic hub.

That last one is the clearest tell. The sanitizer allowlist lives in my own Java, on my own box. A good model will happily guess a plausible allowlist; it cannot know mine. Reasoning is the part I never needed to supply — the model knows what an init container is. The local, arbitrary, load-bearing details are the part nobody else has.

The same split shows up in my CLAUDE.md files. The global one is five lines, and all five are about tone. The one in this project is 250 lines, and it is almost entirely invariants: why LinkedIn's OIDC is hand-wired, why a BeanPostProcessor creates the blog schema, why the agent app must never become a Maven module of the backend. Durable knowledge is project-shaped. Very little of it is global.

The graveyard is mostly capability skills

Looking at the 31 one-shot skills, most of them packaged a capability rather than a constraint: generate slides, build a chart, instrument for a specific vendor, scaffold a contract test. They worked fine. That is precisely the problem — the model did the job on the first ask, so there was nothing for the skill to carry on the second. The install was ceremony.

A few one-shots are honest one-shots. A full security scan of the repo is a thing you do once a quarter, not weekly, and its usageCount: 1 is not a failure.

My MCP servers churned the same way and stabilised on the same principle. What is still wired up points at my systems: my mailbox, a local Kafka broker, a knowledge base, a toy server I built to understand the protocol. The apparent exception is Context7, pinned to one project for library docs — and it is not really an exception. It is also a fact supplier. Just not facts about me: current docs for a library whose API moved after the training cutoff. Same job, different corpus.

Zero hooks, and one fossil

The insights run suggested I add a PostToolUse hook to lint and typecheck after every edit. I have no hooks at all, and I do not think that recommendation is right for my loop — my quality gate already exists, at step 3 of /ship, where a review runs against the full diff before the first commit. Once per branch. A per-edit hook would run it forty times for a signal I want once, and would fire on every intermediate state that was never meant to compile.

Then there is the fossil. My settings contain exactly one permission rule:

{
  "permissions": {
    "allow": ["Bash(gh pr merge:*)"],
    "defaultMode": "bypassPermissions"
  }
}

That allow rule does nothing. It is dead weight under bypassPermissions. It is the residue of a week when gh pr merge kept stalling a pipeline that was already green, so I added a targeted rule — and then went nuclear on the whole permission system and never cleaned up behind me. You can date the layers of a config like rock strata, and I would not have noticed this one without going looking.

The rule I'd give myself in January

Write the skill the second time you explain the same local fact — not the first time you do a task. Doing a task is what the model is for. Re-explaining that the seed script is hard-pinned to localhost, for the third time, is what a file is for.

The corollary is about where it lives. ship.md and dev.md are committed to this repo, so they are correct for this repo and they travel with it. The three that stayed user-scoped are genuinely cross-repo by nature — publishing to my blog, wiring any dockerised Postgres, my own voice. Everything else I installed was homeless, belonging to no particular project and no particular problem.

Homeless is how you end up with 31 skills used exactly once.