When Your Skills Folder Is a Deploy Target, Not a Drawer

Date:

You hardened a skill in the repo, shipped the PR, and the agent kept loading the old one. Or you fixed the live skill by hand, and the versioned copy quietly stayed behind. Same root cause: the copy you run and the copy you version are two different files.

The short answer: The moment you deploy skills — copy them from a repo into the per-user folder the tool actually loads (~/.claude/skills/) — that folder stops being a scratch drawer and becomes a mirror of a versioned source. Now two things can silently disagree: the source can move ahead of the deployed copy (you forgot to redeploy), or the deployed copy can be hand-edited ahead of the source (you edited the mirror). Catch both with two cheap, non-blocking checks: a drift guard (does the deployed copy match its source?) and an orphan guard (is a deployed skill backed by any source at all?). Rule of thumb: edit the source, deploy to the mirror, never the other way round.

A companion piece — Where Claude Code Skills Live — makes the first move: a skill worth keeping gets committed into the repo whose domain it serves, instead of living only in your personal folder. This article is about what happens after you do that at scale, when a deploy step pushes those repo-owned skills back into the folder the tool loads on every machine. That convenience introduces a classic distributed-copy problem, and it’s worth naming before it bites. (A skill is just a folder — a SKILL.md and optional scripts/ and references/, per Anatomy of a Claude Code Skill and the official skills documentation — so none of this is exotic; it’s two copies of some files.)

Edit the source, deploy to the mirror — never the other way round.

Source, deploy, mirror

Three roles, and the bugs all come from confusing them:

  • Source — the skill’s folder inside a repository, under version control. The only place you should edit.
  • Deployer — a step (a hook, a script, a CI job) that copies each repo’s skills into the per-user skills folder so the tool finds them everywhere.
  • Mirror — the per-user folder the tool actually loads from (~/.claude/skills/<name>/). A copy. Not authoritative.

When there’s no deployer, the per-user folder is just scratch and the earlier advice is enough: commit the keepers. But the instant a deployer exists, the per-user folder is downstream of the source — and a copy that nothing reconciles will drift from its original the first time either side changes alone.

Source, deployer, mirror — and the two guardsEdit the source; deploy to the mirror; never the reverse.
Source

Repo folder, version-controlled. The only place you edit.

Deployer

Hook / script / CI. Copies each repo's skills → mirror.

Mirror

~/.claude/skills — what the tool loads. A copy, not authoritative.

Drift guard: does the mirror still match its source? Orphan guard: is a deployed skill backed by any source at all?

A copy that nothing reconciles drifts from its original the first time either side changes alone.

The two ways a deployed skill drifts

There are exactly two failure directions, and they need different fixes:

  1. Source ahead of mirror. You improved the skill in the repo, merged it, and forgot to redeploy. The tool keeps loading the stale deployed copy — your fix is live in git and invisible in practice. Fix: redeploy.
  2. Mirror ahead of source. You edited the deployed copy directly (easy to do — that’s the one the tool is loading while you work), and the versioned source never got the change. The next deploy silently overwrites your edit, and in the meantime the reviewed copy disagrees with the running one. Fix: port the hand-edit back into the source, then redeploy.

Both are the same disease — two copies, nothing watching — and neither announces itself. The deploy “succeeded”; the skill “works.” The only symptom is that the copy under review and the copy in use have quietly diverged.

Guard one: drift (deployed copy vs its source)

For every skill a repo sources, compare the versioned files against the deployed copy, content-only (normalise line endings and trailing whitespace so a Windows checkout doesn’t cry wolf). Report — don’t block — when the deployed copy is stale (differs) or missing (never deployed). Don’t assume a direction: “these two disagree” could be either failure above, so the message says reconcile, and names both fixes (redeploy, or port-back-then-redeploy), rather than overwriting blindly.

Wire it into the same pre-push nudge group as your other drift checks (shared tools, generated workflows). It’s a reminder, never a gate: a guard that blocks a push because a deploy is one commit behind would make being slightly out of sync mean being unable to work.

Guard two: orphans (deployed copy with no source)

The drift guard has a blind spot by construction: it only looks at skills a repo sources. A skill created straight in the mirror — the easiest mistake, because that’s where it runs — has no source anywhere. It’s untracked, unreviewed, unbacked-up, and the drift guard can’t see it because there’s nothing to compare against. It will be lost on the next clean deploy or a machine reinstall, exactly like a personal-folder skill, except now it’s hiding in a folder you thought was managed.

So add the missing-direction check: walk the mirror, and for each deployed skill, confirm some repo sources it. Flag any that none do. To decide “no source,” enumerate the repos you actually have (siblings in your workspace, plus any extra roots you configure) and collect the skills each one sources; a deployed skill in none of those sets is an orphan. Print the fix — give it a home in a repo and commit it — and make it opt-out per skill for the rare legitimately-external case. Non-blocking again; it’s a standing nudge until the orphan gets a source.

A skill created straight in the mirror has no source anywhere — untracked, and the drift guard can’t even see it.

When we first ran this, it immediately surfaced a real one: a genuinely useful skill that had only ever existed in the mirror. Nothing was lost — but one reinstall and it would have been, and no existing check had been watching that direction. (Tracking is a separate question from whether a skill even fires when it should — for that, see Make a Claude Code Skill Trigger Reliably.)

Why both, and why non-blocking

Drift and orphan are duals. Drift watches known skills (sourced somewhere) for a copy that disagrees. Orphan watches the mirror for a copy with no source to agree with. Together they close the loop: every deployed skill is either reconciled with a source or flagged for lacking one.

They’re reminders, not gates, on purpose. A blocking check here would punish normal workflow (you will push before you redeploy) and train people to bypass it. A push-time nudge that fails open — stays silent when it can’t reach a repo, never changes the exit code — gets read and acted on, which a false wall never does.

Correction (2026-10-07): an earlier version of this article called drift a hygiene problem, “not a correctness emergency”, because a skill one commit out of sync still works. That holds for cosmetic changes. It fails for skills that encode rules: a session once ran a whole day on a judging skill that was missing a fix merged hours earlier, and the push-time reminder fired only after the work was done. Keep these guards for hand-edits and missed redeploys, but stop a stale copy from being used at the start of the session.

The short version

Deploying skills from repos into the per-user folder the tool loads turns that folder into a mirror, and a mirror drifts from its source the moment either side changes alone. Edit the source, deploy to the mirror, never the reverse. Run two non-blocking checks: a drift guard (deployed copy vs its source — stale or missing, reconcile in whichever direction) and an orphan guard (a deployed skill no repo sources — give it a home). Both fail open and nudge at push time. That’s the whole system: one source of truth, a deploy step, and two cheap watchers that keep the copy you run honest with the copy you version.

Frequently Asked Questions

Isn’t the personal skills folder just scratch? Why guard it?

It is scratch — until you add a deployer that copies repo-owned skills into it. After that, the same folder holds deployed copies that are supposed to match a source. Scratch and deployed now live side by side, and only the guards tell them apart.

Why not just block the push until everything is in sync?

Because being one commit out of sync is normal, not broken — you routinely push before you redeploy. A blocking check would fire constantly and get bypassed. A non-blocking, fail-open nudge is read and acted on; a wall is routed around.

How does the orphan check know a skill has “no source”?

It enumerates the repositories you have (typically siblings in your workspace, plus any extra roots you configure), collects every skill each repo sources, and flags any deployed skill that appears in none of them. A source in a repo it can’t see is a false positive — cheap to silence with a per-skill allow entry or an extra configured root.

I edited the deployed copy and it works — am I done?

No. That edit lives only in the mirror; the versioned source is behind, and the next deploy will overwrite your change. Port the edit back into the source, commit it, then redeploy. “It works right now” and “it’s kept” are different states.

Where should an orphaned skill be homed?

In the repo whose domain it serves — a pipeline skill with its pipeline, a content skill with the content repo, a cross-cutting one in a shared-tooling repo. Homing by domain keeps the tool next to the code it’s about and puts it under that repo’s review and history.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.