skill-drift
Reviews the skill-drift log that the skill-drift mod writes, and turns skills that keep drifting across sessions into draft fixes.
npx skills
npx skills add JoshuaOliphant/compost --skill skill-drift
Works for Codex, Cursor and other harnesses. Use -a <agent> to target one, e.g. -a codex.
Claude Code
/plugin marketplace add JoshuaOliphant/compost
/plugin install compost@compost
skill-drift
A skill drifts when the way it gets used stops matching what it says: the agent skips a step it
states, it leaves out something the agent then has to work out, or the user corrects work it
covers. This is not upstream drift, which compost:turn handles; that is compost's copy falling
behind a source.
The skill-drift mod, which ships with compost (hooks/skill-drift.ts), collects the evidence.
After each turn that loaded a skill it asks the decision model (TypeSafe Jev, or the model a local
Ollaya server names in TYPESAFE_BASE_URL and TYPESAFE_DEFAULT_MODEL) whether the turn drifted,
and writes each hit to ~/.claude/skill-drift/<date>-<session>.jsonl with the turn's prompt, a
trace excerpt, and a hash of the skill's text. It costs one Jev request per skill loaded in a
turn, and one more per skill when the next prompt is checked for a correction. It never interrupts
a session, and without a TypeSafe key or an Ollaya server it asks and writes nothing. It loads
only where Claude Code runs plugin hooks modules (CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 while they
roll out), so an empty log can mean the mod is not running: say so in the digest rather than
reporting a clean week. This skill reads that log, usually from a daily scheduled task, and
proposes fixes.
One hit proves little: a turn can stray for reasons of its own. The same kind of drift on the same skill in two separate sessions is a pattern, and patterns are what this skill acts on. When the pattern is mechanical, a check stops it better than more words in the skill (make mistakes impossible).
${CLAUDE_PLUGIN_ROOT}/scripts/skill_drift.py does the bookkeeping (Python 3.11+ via uv, stdlib
only). It drops hits on skills that are no longer active (a disabled plugin, a skillOverrides
entry set to off, a removed personal skill), reading plugins from claude plugin list --json
as compost:setup's replaced check does, and counts only hits on each skill's latest version,
so a skill already fixed stops producing patterns from its old text.
Review the log
- From the marketplace checkout, run
uv run ${CLAUDE_PLUGIN_ROOT}/scripts/skill_drift.py patterns. It prints JSON: each pattern's skill, kind (deviated,missing_guidance,corrected), owner, skill file, session count,earlier_versions(hits on the skill's text before its last change), and the hits with theirevidence,prompt, andtranscriptpath. With no patterns, report theretiredandunreadablecounts, then check that the mod could have run, because a quiet log and a dead mod look the same:CLAUDE_CODE_ENABLE_FUNCTION_HOOKSis1in the environment or in theenvblock of~/.claude/settings.json, compost's installed version shipshooks/skill-drift.ts, and a TypeSafe key exists inTYPESAFE_API_KEYor, failing that, in thetypesafekeychain entry (security find-generic-password -s typesafeexits 0), orTYPESAFE_BASE_URLnames another server such as Ollaya (which needs a key only if it setsOLLAYA_API_KEY). Name the check that failed, or say all three passed. A scheduled run cannot prove the module loaded; when every check passes, call it a quiet stretch. Then go straight to the digest in step 6. - Read each pattern against the skill. Open the skill file and read every hit's evidence next to the step it concerns. When the excerpt is too thin, search the hit's transcript for its prompt and read that turn. A pattern whose hits describe different things is not a pattern: treat each hit on its own.
- Decide where the fault is, and record the ruling (what, why, cost if wrong):
- The skill. A step is wrong, missing, or unclear, or real use keeps needing something it does not say. The fix goes in the skill.
- The instruction files. The skill is right for its purpose but
CLAUDE.mdorAGENTS.mdtells the agent otherwise, so the agent follows one and breaks the other. Name the conflict and propose which side changes. - The agent. The skill is clear and the instruction files agree; the agent ignored it. No edit, unless the step lacks the reason that would make it stick: then add the reason.
- A false flag. The evidence does not show drift. Note it; enough of these on one kind mean the mod's threshold needs raising.
- Act by owner.
repo, fault in the skill, with a pull request fromclaude/skill-drift-<skill>still open: the fix is in flight, and the new hits are on the text it replaces until it merges and installs. Don't branch again. Read the hits against the open fix; when it already covers them, comment their prompts and evidence on that pull request, and when it doesn't, push the extra edit to its branch. The ruling is "fix in flight", with the link.repo, fault in the skill, nothing open: branchclaude/skill-drift-<skill>from the default branch, edit the skill file, and when the skill is compost's run the regression checks in turn's "Check compost's own text after a change". Bump the plugin's version asAGENTS.mdsays, run the repo checks, commit (fix(<plugin>): <what the skill now says>), push, and open one draft pull request per skill whose body carries the ruling and each hit's prompt and evidence. It stays a draft for the user to merge.- Everything else (
personalandthird-partyskills, instruction-file conflicts): write the proposed change, as exact text, in the digest. Never edit files outside the checkout, and never editCLAUDE.mdorAGENTS.mdfrom a scheduled run. - Mark what you handled:
uv run ${CLAUDE_PLUGIN_ROOT}/scripts/skill_drift.py mark <id>...with theidof every hit you ruled on, whatever the ruling, so the next run starts clean. - Write the digest to
~/.claude/skill-drift/digests/<date>.mdand end with the same text: one line per pattern with its ruling and outcome (pull request link, proposal, or no change), the retired and unreadable counts, and any skill whoseearlier_versionsis non-zero and has drifted again, since that means its last fix did not hold. Every run writes one, including a run with no patterns, whose digest also carries step 1's check results. List the open hits below the two-session bar fromuv run ${CLAUDE_PLUGIN_ROOT}/scripts/skill_drift.py summary, with no ruling, and leave them unmarked:patternsleaves them out, and a later session can still make them a pattern.
Run it on a schedule
Make it a local scheduled task in Claude Desktop (Routines, New routine, Local), not a remote routine: the log lives on this machine, and a remote routine only sees a fresh clone.
- Instructions:
/compost:skill-drift - Folder: the marketplace checkout, with the worktree toggle on so each run starts clean.
- Schedule: daily. A day the machine slept through gets one catch-up run on wake.
- Permission mode: one that edits without asking. Click Run now once, answer each prompt with "always allow", and later runs go through unattended.
Each run appears under Scheduled in the sidebar with a notification; the digest is its last message.
Next moves
compost:buildwhen a proposed fix is bigger than an edit to one skill's text: file it as an issue and build it.compost:turnwhen a pattern shows compost's copy of a skill drifted because its upstream source already fixed the same thing.compost:retrowhen the drift traces back to the environment rather than the skill: a missing check, a hidden file, a rule in the wrong instruction file.