rune
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.
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 -freturns 0 on a 304 without writing the requested file, so a stale-cache response fell through intotar xzfon a file that didn’t exist, producing an opaque BSD tar error. The fix replaced the shell-out withureq+tar+flate2in Rust and a typedArchiveResponseenum (Fresh/NotModified/StaleOk) that structurally cannot reachextract()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 (
--registryfor what was actually--from,--typefor-t). v0.15 shipped the aliases that made those guesses correct instead of wrong. - Renaming a registry in
config.tomlused to break every project still holding the old name, withUnknown registry: Xand no automatic recovery. Analiaseslist 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
.rsfile over 500 lines, which is what actually forcedsrc/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.