Your agent followed last week’s version of a skill all day. The drift check only told you when you pushed.
If you keep agent skills in git and copy them into the folder the tool loads (for Claude Code, the per-user skills folder described in the skills documentation), the repo is the source and the loaded folder is a mirror. When Your Skills Folder Is a Deploy Target, Not a Drawer covers that model and two push-time checks for it. A push-time check still leaves one gap, and it is the expensive one.
A drift warning that arrives after the work is done is a postmortem, not a guard.
Table of Contents
Why push time is too late for a stale deployed skill
A typical drift check compares each skill in the repo with its deployed copy and warns, at push time, when they disagree. That timing looks fine at first, since a skill one commit behind still runs.
Here is how it goes wrong. A skill that reviews records and logs a verdict for each one gets a fix to how it writes those verdicts. The fix merges, and nobody re-runs the deploy step. The next morning a session invokes the skill and reviews records for a full day using the deployed copy, which lacks the fix. The drift warning fires that evening, when the session pushes. Every verdict logged before then followed rules that had already been corrected.
The skill's rules are corrected in the repo and reviewed.
Nobody re-runs the deployer. The copy the agent loads stays old.
A session runs the old skill. Every result follows the superseded rules.
The drift reminder fires, after the work is done.
A session-start heal moves the check before the session uses the skill: re-sync from the default branch, refuse a hand-edit.
So the question is when you check, not whether. Some skills are formatting helpers, and one commit of drift is cosmetic. Others encode judgment rules, data contracts or guard rails. For those, a stale copy is wrong in exactly the way the fix was meant to prevent, and you pay for everything the session did before anyone noticed.
Why “just redeploy everything first” backfires
The obvious patch is to make every skill run your full deploy script before it starts. That fails on three counts:
- It deploys the wrong content. A typical deploy script copies each repo’s checkout as it sits on disk: whatever branch is checked out, uncommitted edits included. Run from a session, it can push an unreviewed branch to every other session on the machine.
- It overwrites hand-edits. A copy someone edited directly in the deployed folder is lost with no warning.
- It is too big. A full deploy rewrites every skill, and often your git hooks too, to refresh one file.
What a session needs is narrower: this one skill, from reviewed content, and only if replacing it loses nothing.
Heal one skill from the default branch only
The heal reads the skill straight from git objects on the repo’s default branch (origin/main after a fetch), never from a checkout. That one choice gives the right behavior:
- Reviewed content only. What deploys is what merged. A change on an unmerged branch is not live until it merges, which is what you want for anything shared.
- Immune to local mess. Uncommitted edits, a feature branch, or a checkout that is days behind can’t leak into the deployed copy.
- Scoped to one skill. It writes only inside that skill’s deployed folder, with no hooks and no neighbors touched.
Treat name collisions with the same caution. If two repos both provide a skill with the same name, the heal refuses rather than picking one.
Deploy what merged, not what happens to be checked out.
Stale or hand-edited? A deploy manifest tells them apart
Comparing the deployed copy with the default branch tells you that they differ, not why. “Differs” covers two very different situations:
| Deployed copy vs default branch | Deployed copy vs its manifest | Meaning | Heal does |
|---|---|---|---|
| equal | (not checked) | current | nothing (silent) |
| differs | equal | merely stale: nobody touched it since the last deploy | re-sync this skill, write a new manifest, tell the session to re-read it |
| differs | differs | hand-edited in the deployed folder | refuse; port the edit back to the repo |
The manifest is a small file your deploy step writes into each deployed skill folder. It holds a content hash per file, plus the source repo and commit. It works like fingerprinting a record at import: hash what you copied when you copied it, so you can tell later whether anyone changed it. Hash normalized content (line endings and trailing whitespace stripped) so a Windows checkout never looks like an edit.
Two edge cases need a rule, and both lean conservative:
- No manifest yet, for a copy deployed before you added manifests. Treat it as stale only if it equals some committed version of the skill, so it can be reproduced from history. Otherwise refuse.
- The fetch failed. Never write. If the deployed copy matches the last-fetched default branch, carry on with a warning; if not, refuse, because “current” can’t be confirmed.
One rule sits under all of these: a heal deletes only what it can prove is a deployed copy. Anything uncertain becomes a refusal with a reason, and the refusal tells the session to read the skill from the repo by path for now.
Put the heal at the top of every skill
A heal nobody runs is just another reminder. So make it the first section of every skill you keep in a repo, straight after the title, calling whatever small script implements the steps above:
python path/to/heal_skill.py <skill-name>Code language: Bash (bash)
Have that section say what each result means: silent (current, proceed), healed (the session loaded the old text, so re-read it), or refused (don’t trust the loaded copy; read the repo version by path and tell the user). A local lint that flags any skill whose first section isn’t the heal stops new skills from quietly skipping it.
Two refinements close what remains:
- Heal everything at session start, in the background. A session-start hook can launch a heal of all skills, fetching each source repo once, without blocking startup. The next session start reports any refusals from that run.
- Serialize heals. Sessions often start in bursts. One machine-wide lock means two heals never write the same folder at once, and a skill invoked mid-heal simply waits, then finds itself current.
Is a stale skill ever harmless?
Sometimes. If a skill’s changes are cosmetic (wording, formatting, examples), one commit of drift costs nothing, and treating drift as tidiness is reasonable. That reasoning breaks for skills that encode rules, and those tend to be the ones that matter most. Keep the push-time drift warning, because it still catches hand-edits and deploys nobody ran. The protection against running a stale skill belongs at the start of the session, where it can still change what the session does.
The short version
A stale deployed skill costs a whole session, because a push-time check reports it only after the work is done. Heal at the start instead: re-sync the one skill from the repo’s default branch, never from a working tree. A per-skill deploy manifest lets the heal replace a merely stale copy and refuse a hand-edited one. When unsure (no manifest and no matching commit, or an unreachable remote), refuse and point the session at the repo copy. Put the heal first in every skill, lint for it, and run a background heal at session start so the per-skill check is nearly always a no-op.
Frequently Asked Questions
Why not block the push until the deployed skill is current?
The push isn’t where the damage happens. By push time the session has already used the stale skill, so a blocking check punishes the push without undoing the work. Healing at the start prevents the damage instead.
Won’t healing from the default branch hide my unmerged skill change?
Yes, on purpose. An unmerged change is unreviewed, so it shouldn’t reach every session on the machine. Test it on its branch by pointing the session at the repo file by path. It goes live for everyone once it merges.
How do I find deployed skills that no repo provides at all?
A heal only looks at skills it can trace to a source, so it won’t notice one that was copied in by hand and never committed. List the deployed folders, subtract every skill name your repos provide, and review what is left before deleting anything.
What does the heal cost at session start?
Mostly one network fetch per source repo, a few seconds each, and they can run in parallel. Running it in the background keeps startup instant, and a skill invoked mid-heal waits on the lock rather than racing it.