Skip to content
andrew.dunn.dev

rune

Source

rune was a Rust command-line tool that managed three kinds of markdown living under a project’s .claude/ directory: skills (workflow instructions an agent reads to perform a task), subagent definitions, and rules (conditional instructions scoped to a context). It treated a git repository, a “registry” in rune’s vocabulary, as canonical, and every project’s .claude/skills/, .claude/agents/, and .claude/rules/ as a synced working copy of it, with a lockfile playing the same role a package manager’s lockfile plays, except the packages were prompts an agent would read mid-session.

The problem it solved came from running Claude Code across many projects at once: a skill corrected in one project’s session had no path back to the other projects that should see the fix, and no record of which copy was current. rune’s add / sync / check / push commands gave that a git-shaped answer, and a content-hash lockfile turned “did this drift” into a fact rather than a guess: CURRENT, DRIFTED (with direction), MISSING, or REGISTRY MISSING.

rune push, the only way backCANONICALregistryone git repoPER PROJECT.claudesynced copyON EVERY WRITEhook firessame turnLOCKFILErune checkcopy vs pinnedDRIFTfour statesnamed exactly
One git registry held the skills, every project carried a synced copy, and a write hook named the drift in the same turn, with push the only way back.
rune syncon synca write landscompared againstrune checkrune push, the only path back to the registryREGISTRYa git repositorycanonicalskills, agents, rulespinned per item to a SHAWORKING COPYone synced copy.claude/skills, agents,and rules, per projecta commit shipped nothingLOCKFILE.claude/rune.lockper item: content hash,source registry, and theregistry-side commitPOSTTOOLUSE HOOKfires on every writeunder .claude/skills,agents, or rulesdrift in the same turnFOUR DRIFT STATESexact, not heuristicCURRENT, MISSINGDRIFTED (with direction)REGISTRY MISSING
Sync carried registry items into a project's .claude directory and wrote a lockfile, a hook ran rune check on every write to return one of four exact drift states, and push was the only path back.

Highlights

  • The lockfile (.claude/rune.lock) recorded content hash, source registry, and registry-side commit per item, which is what made the four drift states exact rather than heuristic.
  • A registry could be pinned per item to a tag, branch, or commit (name = "registry@v1.2.0"); rune resolved it to a SHA and enforced that SHA on every later sync, so a moved tag surfaced as a hard error instead of silently changing what got installed. Archive-source registries (tarball downloads, no git history) could not support this and said so up front rather than pretending to pin.
  • A three-pass adversarial security review closed path-traversal and TOCTOU-adjacent findings across two follow-up passes before v0.3.0 shipped, the kind of review a personal CLI rarely gets and probably should more often.
  • The archive fetch path had a real production bug: curl -f returns 0 on a 304 without writing the requested file, so a stale-cache response fell through into tar xzf on a file that didn’t exist, producing an opaque BSD tar error. The fix replaced the shell-out with ureq + tar + flate2 in Rust and a typed ArchiveResponse enum (Fresh / NotModified / StaleOk) that structurally cannot reach extract() on a 304.
  • rune ran its own CLI through an LLM-usability harness borrowed from the jig methodology: 50 trials, two models, a synthetic fixture of registries and drifted projects. The baseline came back at a 0.55 mean score and 84% completion, with agents inventing flags that felt right but weren’t (--registry for what was actually --from, --type for -t). v0.15 shipped the aliases that made those guesses correct instead of wrong.
  • Renaming a registry in config.toml used to break every project still holding the old name, with Unknown registry: X and no automatic recovery. An aliases list on the registry entry let old references resolve until each project’s next sync rewrote them to the canonical name.
  • A CI gate blocked any .rs file over 500 lines, which is what actually forced src/registry.rs (1197 lines) apart into nine focused submodules rather than leaving it to happen “later.”
  • MIT licensed.

The mechanism that seemed worth keeping, independent of whether rune itself stuck around, was surfacing drift at the moment a file changed rather than at the next scheduled check, and making the authoritative copy explicit instead of assumed.


Created Apr 2026 · Archived Jun 2026
Retrospective →