Agent-First Data v0.11: The Skill Installer, in Four Languages
A spore can ship its own Agent Skill — but getting that SKILL.md into Codex, Claude Code, and opencode is fiddly, per-agent filesystem work. v0.11 adds run_skill_admin: install, uninstall, and status for an embedded skill, with the same behavior and byte-identical output across Rust, Go, Python, and TypeScript.
A tool that an agent drives is more useful when it ships the instructions for
driving it — an Agent Skill, a SKILL.md the coding agent loads and follows. But
shipping the file is the easy half. Installing it is the fiddly half: each agent
keeps its skills in a different place, the directory differs again between
personal and project scope, and a careless write clobbers a skill the user
hand-edited. Every spore that wanted to install its own skill was about to
reimplement the same path-juggling, the same overwrite guard, the same “is this
copy current?” check.
v0.11 makes that a library call instead. run_skill_admin installs, uninstalls,
and reports status of a spore’s embedded skill across Codex, Claude Code,
and opencode — and it does the same thing, the same way, in all four
languages.
One call, three agents, two scopes
You describe the skill once and pick an action:
run_skill_admin(spec, "install", { agent: "all", scope: "personal" })
spec carries the skill name, the bundled SKILL.md, a title, and a short
marker slug. agent is all or one of codex / claude-code / opencode;
scope is personal or project. The library knows where each agent looks —
~/.codex/skills, ~/.claude/skills, ~/.config/opencode/skills for personal,
the project’s .claude/skills and .opencode/skills for project — including the
CODEX_HOME and XDG_CONFIG_HOME overrides. Codex has no project scope, so
--agent all --scope project simply skips it rather than failing.
It won’t clobber what it didn’t write
Install stamps two marker comments into the file — a “generated by” line and a
managed-skill marker keyed to your slug. Those markers are how the installer
tells its own file apart from one a user wrote by hand. If a skill already
exists at the target path and it carries neither the markers nor byte-identical
bundled content, install and uninstall refuse to touch it unless you pass
--force:
refusing to overwrite unmanaged skill at ~/.claude/skills/agent-first-widget/SKILL.md
hint: pass --force to replace it, or choose another --skills-dir
The default is conservative on purpose. The installer manages what it created and leaves everything else alone.
status knows when a copy is stale
The interesting field in status is current. It is true only when the
installed content matches the skill the binary ships right now — markers
stripped, blank runs normalized, then compared. Upgrade the tool and the bundled
skill changes; the copy on disk does not. current flips to false, and
re-running install refreshes it. No version field to read, no manual diff: the
content is the source of truth.
{"code":"skill_status","skill":"agent-first-widget","current_all":false,
"targets":[{"agent":"opencode","installed":true,"valid":true,"managed":true,"current":false}]}
status reports installed, valid (front matter parses), managed, and
current per target, plus the rollups — so an agent can look once and know
whether to install, refresh, or do nothing.
A structured result, not a bag of JSON
run_skill_admin returns a typed report, not a pre-serialized blob. You read
its fields directly, or you serialize it yourself — whichever the calling code
needs. Each port uses the shape that is idiomatic for the language:
- Rust — a
code-taggedSkillReportenum - Go — a sealed
SkillReportinterface overSkillStatusReport/SkillInstallReport/SkillUninstallReportstructs - Python — a
SkillReportdataclass withto_dict() - TypeScript — a discriminated union you narrow on
code
The library never writes to stdout or stderr; rendering stays the caller’s decision, which is the same contract the rest of AFDATA’s output layer keeps.
The same, in four languages
This is the part that took the work. The generated SKILL.md — markers, spacing,
and all — is byte-identical across Rust, Go, Python, and TypeScript, and so is
the serialized report. The front-matter validator, the managed-marker logic, and
the normalize-then-compare freshness check are hand-ported rather than wrapped, so
there is no dependency to drift and no per-language YAML quirk to reconcile. The
test suites mirror each other case for case, and a cross-language diff pins the
output.
It is filesystem and CLI tooling for the spore binaries, not part of the
cross-language data convention — so in Rust it lives behind the skill-admin
cargo feature, and ships as a normal module in the Go, Python, and TypeScript
ports.
The agent rule
If your tool ships a skill, let the tool install it:
- describe the skill once with a
SkillSpec; callinstall/uninstall/statusfor the rest - trust
currentto decide when to refresh — content, not a version string - let the markers guard the user’s own files; reach for
--forceonly when you mean it - read the typed report, or serialize it — never parse stdout
A spore can now carry its own instructions and put them where the agent will find them, the same way on every runtime it supports.