Skip to content
Joost.blog
Illustration for: How I made my skills update themselves

How I made my skills update themselves

·5 min read

I updated one of my Agent Skills today and realized I had no way to tell my other machines they were running a stale copy. Skills install as loose folders in ~/.claude/skills/ (or wherever your agent puts them). There’s no npm outdated, no brew upgrade, no update daemon. The skill just runs whatever’s on disk.

So I added a small pattern that makes each skill check itself on invocation. If it’s out of date, the skill offers to install the update before continuing. I couldn’t find anyone doing quite this. The whole thing is a few lines of configuration per skill.

Why versioning matters when posts and skills ship together

Most of my skills are the executable companion to a post:

Each pairing turns an opinionated blog post into a drop-in workflow.

This is a shipping pattern I like a lot: the post argues for a set of choices, the skill makes those choices the default. Want to understand why? Read the post. Want to apply it? Run the skill.

But opinions evolve. When I update the Astro SEO guide to cover a new build-time validator, I update the skill in the same pass. If users don’t know their installed skill is behind the post, they’ll follow instructions that no longer match what I’d write today. An Agent Skill without a version check is cached documentation. Useful until it’s wrong. And you don’t find out until it is.

That’s the specific problem I’m solving. The pattern below is general, but this is why it matters to me.

The pattern

Four pieces.

First, every SKILL.md carries a version: field in its frontmatter:

---
name: astro-seo
version: "0.4"
description: >
  Audits and improves SEO for Astro sites...
---

Second, a single versions.json at the repo root maps each skill name to its current version:

{
    "astro-seo": "0.4",
    "readability-check": "0.4",
    "github-repo": "0.3"
}

Third, every SKILL.md includes a short paragraph that tells the skill to self-check when it runs — and, if the user approves, to install the update inline:

Before running, fetch https://raw.githubusercontent.com/jdevalk/skills/main/versions.json
and compare the `astro-seo` entry to the `version:` in this file's frontmatter.
If the manifest version is higher, tell the user the skill is out of date and
offer to update it now. If they agree, run:

    curl -fsSL https://github.com/jdevalk/skills/releases/latest/download/astro-seo.skill \
      -o /tmp/astro-seo.skill \
      && unzip -oq /tmp/astro-seo.skill -d <parent of this skill's directory> \
      && rm /tmp/astro-seo.skill

After the unzip, ask the user to re-invoke the skill so the new version loads
into context. The check is informational and never blocks: if the user
declines, continue with the rest of the workflow on the current version.

Fourth, a CI job that fails any PR where a skill’s frontmatter version doesn’t match its versions.json entry. The manifest and the shipped skills can’t drift.

That’s it. No runtime. No service. The skill checks on each invocation using the agent’s own WebFetch tool. If the user approves the update, the skill re-uses the Bash tool that’s already there to pull the latest release and unpack it in place. Users find out they’re behind — and can be caught up — at exactly the moment it matters: when they’re about to run the skill.

Why no cache

The obvious concern is that every skill invocation now costs a network round-trip. Three reasons I left it alone:

The WebFetch tool already has a built-in 15-minute cache. Multiple skill invocations in one work session share the response, which covers the realistic hot path. I’m not going to beat that with hand-rolled caching.

A longer cache fights the purpose. The whole point of the check is to alert me about updates. A 24-hour cache means I could miss a release for a day. The check is already non-blocking. A cache miss costs less than running a stale skill for another day.

The fetch is around a hundred milliseconds against a static GitHub raw URL. In a human-paced workflow where I invoke a skill once or twice per session, it’s noise. If I ever ship fifty skills and it matters, the better fix is to centralize. One shared manifest fetch per session, not one per skill. But that needs harness support Claude Code doesn’t expose to skill authors today. So the skill is the smallest unit with an execution context, and that’s where the check lives.

What I couldn’t find

Before writing this up I went looking for prior art. The closest patterns in the Agent Skills ecosystem:

  • Anthropic’s marketplace.json plus /install: version tracked in a marketplace manifest, updates happen via an external CLI command or an update_marketplace.py script. The user has to step out of whatever they were doing to learn they’re behind.
  • skills-updater (community skill): scans your installed skills, checks each against its remote repo, and offers to update them. The closest prior art I found. But it’s a separate skill you remember to run, not a check embedded in the skill you’re actually trying to use.
  • Smithery, OpenCode, and other skill hosts: track versions for distribution but don’t ship a self-check.

None of them put the check — and the install — inside the skill itself, at invocation time, against a repo-local manifest. Which is fair: it’s not a big idea. It’s just a pattern that package managers have had for decades, applied to a new distribution format that doesn’t have one yet.

If you maintain skills and your users install them as loose files, steal this. It works for any agent whose skill format is “a folder with a markdown file and some frontmatter.”

Have you seen a better workflow for keeping agent skills in sync with their source? I’d genuinely like to know. This is the best I could come up with, but that’s a low bar when the format is new enough that conventions haven’t settled.

Thoughts, corrections, or something to add? Reply on Bluesky or LinkedIn.

Share
Esc