capshelf
SkillAI & modelsUse the capshelf CLI to manage shared skills, Pi extensions, subagents, settings, and MCP configs across multiple projects from a user-owned data repo.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the capshelf skill
What this skill tells your AI
The instructions your AI receives, as published by genged/capshelf in .agents/skills/capshelf/SKILL.md and read by ahel’s review.
This project uses capshelf to track shared coding-agent config (skills, project-local Pi extensions, Claude/Codex subagents, settings fragments, MCP configs) pulled from a data repo. When the user asks to add, remove, discover, edit, or update shared config, use the capshelf CLI. Do not hand-edit .capshelf/capshelf.json or .capshelf/capshelf.lock.json — they are tool-managed.
Run project commands from anywhere inside a capshelf project — the directory containing .capshelf/capshelf.json, or any subdirectory of it (capshelf walks upward to find the root, like git). init acts on the current directory, not a discovered parent.
Use init only for a new project or a fresh clone without
.capshelf/local.json; it refuses a project already initialized on this
machine. Use data bind, data upstream, and update for later lifecycle
changes.
The agent decision loop
Work the shelf in this order instead of pattern-matching on bare item names:
1. Survey the project
Run capshelf status at session start to see drift and available updates before changing anything. capshelf ls --here lists what is already installed (with descriptions and #tags when the data repo declares them).
2. Discover on the shelf
Reach for capshelf search <task words> first — it matches names, tags, descriptions, and item content across the bound data repo plus bundled system items, ranked by relevance:
capshelf search "sql injection"
capshelf search security --json
Zero matches exit 0 — an empty answer is a valid answer, not an error. To browse instead, use capshelf ls --tag <tag> (repeatable, AND) or ls --kind <kind>. Descriptions and tags are the selection signal.
Results with a bundles/ prefix are bundles — curated item sets. Prefer them when setting up a project: capshelf show bundles/<name> to preview members and install state, then capshelf add bundles/<name> to expand. Expansion is traceless (members become ordinary independent items); the bundle field in add --json is the only echo and is what belongs in a commit message.
3. Evaluate before installing
capshelf show <item> prints the full description, requires, conflicts-with, and whether each relation is already installed. Use it before committing to an add. For pi-extensions/*, read all extension source shown before adding: Pi executes it as arbitrary code with full user permissions after project trust.
4. Install
capshelf add <item>. For mcp/* and subagents/*, read the targets: block
add prints and tell the user which runtimes the item covers — see Target
coverage. Repeating add for an installed item is a stable no-op; use the printed status --diff, update, and apply guidance instead of trying to make add reapply it. If the output lists missing required items, install them with the exact capshelf add <ref> commands it prints. If add refuses with exit 3 because of a conflicts-with declaration, that is a curated incompatibility — surface the decision to the user (remove the conflicting item, or fix a stale declaration in the data repo); never work around it. A bundle preflight refusal (exit 3) is the same kind of decision: nothing was installed and the per-member report says why — surface it, don't install members one by one to route around it.
5. Verify
capshelf status --strict — exit 0 means the project has converged on its locks.
6. Edit / promote loop
When the user asks you to improve a shared (data) item:
capshelf get-path <item>for the absolute editable path (fragments return canonical data-repo source files;--outputreturns generated outputs for inspection only).- Edit with your Edit/Write tools.
capshelf status <item>— should reportdrifted_local(orsource_dirtyfor fragments).- Decide with the user:
capshelf promote <item> -m "why"— push to the data repo. Other projects seeupdate availablenext time they check; nothing auto-changes.capshelf keep-local <item> --reason "why"— intentional project-specific divergence for a copy item (skill or Pi extension).capshelf revert <item>— discard the edit, restore from the recordedsourceCommit; first showcapshelf status <item> --diffand get permission before using--yesin a non-interactive run.
For Pi extensions, inspect the changed source before promoting and tell the user to run /reload or restart Pi after materialization. Never imply that capshelf reviewed, trusted, sandboxed, or dependency-installed the extension.
If promote fails with "changed in the data repo since this project last updated" (exit 3), a teammate's newer version is upstream. Show the user the upstream diff (capshelf status <item> --diff plus the scoped git log from the error message). For a skill in either scope or a project-scope Pi extension, offer capshelf promote <item> --merge -m "why" to three-way merge the locked base, installed edits, and current upstream. A merge conflict lists paths and writes nothing. Other choices are preserving the edit and running capshelf update <item> before redoing it, or an intentional overwrite. Do not retry with --stale-ok on your own; --merge and --stale-ok are mutually exclusive. update preflights local drift and asks before replacing installed content; non-interactive and JSON calls refuse unless --yes is passed. Review capshelf status <item> --diff and get the user's permission before using update --yes. For a local-scope item, preserve --local on recovery and merge commands. A merged or ordinary promote that reports already-upstream means the lock was re-pinned without a data-repo commit.
To change metadata (tags, description, requires/conflicts-with, or declared needs), edit the item's canonical data-repo sidecar (skills/<name>/.capshelf.yml, pi/extensions/<name>/.capshelf.yml, or the fragment path shown by capshelf show) and commit it in the data repo. Metadata is never hashed into item content. Tags, descriptions, and relations are live catalog data and need no project update; needs are lock-pinned, so consuming projects run capshelf update <item> to select a changed declaration without reinstalling unchanged content. Commit the sidecar before returning to project work: an uncommitted sidecar edit blocks capshelf update entirely (dirty data repo) and blocks add of that item.
For a skill's description, prefer SKILL.md frontmatter — it doubles as the catalog fallback. Know the trade-off when choosing where to edit: a frontmatter edit is content drift (shipped to Claude, hashed — consuming projects see update available), while a sidecar edit is drift-free. Add a sidecar description only when the catalog blurb should differ from the frontmatter's invocation-trigger phrasing, or when tuning copy must not ship a content change; sidecar wins when both exist. Fragment items (settings/mcp/codex-config) have no frontmatter — the sidecar is their only description source.
For system items (e.g. this capshelf skill), the edit loop doesn't apply — to change them, edit the CLI source under src/bundled/ and rebuild.
How it works
- Data repo (e.g.
~/code/work-skills/) holds canonical versions of every shared item underskills/,pi/extensions/,subagents/,settings/,mcp/, andcodex/config/. It must be a git repo. Resolution order:--data <path>flag > gitignored.capshelf/local.json>$CAPSHELF_HOME. There is no implicit default. - This project pins the exact content hash + source commit of each item in
.capshelf/capshelf.lock.json(clone-local pins in gitignored.capshelf/local.lock.json). Data-repo updates do NOT propagate until this project runscapshelf update. - Installed copies live under
.agents/skills/<name>/by default with.claude/skills/<name>symlinks (Claude-only projects install directly under.claude/skills/<name>/). Pi extensions live under.pi/extensions/<name>/. Claude custom commands are modeled as skills. - Subagents are project-scoped logical items.
subagents/<name>/claude.mdinstalls to.claude/agents/<name>.md;subagents/<name>/codex.tomlinstalls to.codex/agents/<name>.toml. Either target or both may exist under one lock; capshelf reports which as target coverage. - Item metadata (optional
<item>/.capshelf.ymlin the data repo:description,tags,requires,conflicts-with,needs) feeds discovery and checks. It is never copied into projects. Needs are pinned separately from content so requirements freshness never changes content drift.
Two kinds of items
- system (lock prefix
system/): bundled into the CLI binary, installed byinit, read-only from a project's perspective. - data (lock prefix
data/): live in your data repo. Added viaadd, removed viarm, adopted viashare, pushed back viapromote.
Mutating commands only touch files tracked in the lockfiles: add refuses to overwrite an existing untracked target, and rm deletes only locked data items. Copy-directory items can use committed project scope or clone-local scope; subagents are project-only. share skills/<name> defaults to local scope; Pi extensions default to project scope, so pass --to local when adopting one as clone-local intent.
Command reference
Always check the current surface with capshelf --help and capshelf <verb> --help. Most item arguments accept a bare unique name (hello) or a kind-qualified ref (skills/hello).
| verb | purpose |
|---|---|
init | initialize a new project or onboard a fresh clone without .capshelf/local.json; never use it to reinstall or rebind an initialized machine |
data bind / data upstream / data path | inspect or change the explicit data-repo binding (old set-data/set-upstream/data-path still work as aliases) |
ls / show / search / status | inspect and discover (all support --json; ls and status include user-level runtime skills by default, --user narrows to them only) |
add / rm / apply / update / revert | converge the project on its locks |
lock migrate | one-time conversion of this project's locks to version 4; required before any lock-writing command works on an older project |
share / move / promote / keep-local | flow content and intent between project and data repo |
data sync [--json] | explicitly fetch the bound data repo's origin and fast-forward when safe; the only capshelf command that touches the network besides the init bootstrap clone and self-update. Run it when the user asks to pick up teammates' changes, then capshelf status to see update_available |
get-path | print the editable path for an item; use `--target claude |
self-update | update the Homebrew-installed binary (not project pins) |
marketplace ... | author, validate, sync, and package data-repo Claude/Cowork or Codex plugin catalogs; never installs runtime plugins |
Lock version 4 migration
Locks written before version 4 identified an item by a hash of the data repo working tree. Version 4 identifies it by the item's committed Git tree. One lock file carries one identity model, never both, so the conversion is an explicit command instead of a side effect of the next write.
On a project whose lock is version 2 or 3, every command that writes a lock
refuses with exit 3: add, rm, update, revert, promote, share,
move, keep-local, bundle installs, and init. The refusal reads:
✗ this project's lock is version 3; capshelf add writes lock version 4
Convert the project and local locks first: capshelf lock migrate (preview it with --dry-run).
status, ls, show, search, get-path, and apply keep working against
the old lock. Because status does not print the lock version, read the
version field of .capshelf/capshelf.lock.json when you must know it before
a write. Read it only; do not hand-edit it.
Convert in two steps:
capshelf lock migrate --dry-run # plan the complete migration, write nothing
capshelf lock migrate # convert both lock files in one transaction
The default conversion selects no new content. For each entry it re-derives
the pin from the commit that entry already names and keeps appliedAt,
needs, label, and the keep-local marker unchanged. A recorded hash that
disagrees with its own commit is reported as repaired legacy identity. The
project lock and the clone-local lock convert together or neither does. On a
project that is already current the command prints ✓ already version 4 and
exits 0, so it is safe to run when unsure.
An entry whose source commit is unreachable, or whose committed content is
missing or filter-refused, blocks the run. Every blocker is reported in one
pass, the command exits 3, and no lock or installed file changes. Prefer the
non-destructive fix: restore the commit (capshelf data sync, or push the
clone that holds it), then retry. The repair flags are the user's decision, not
yours:
--repin <ref>re-pins a copy item or subagent to its current committed source and re-materializes it. That is an update, not a conversion — the installed content changes.--remove-item <ref>drops the entry. It is the only choice for a fragment (settings/,mcp/,codex-config/): a fragment's former contribution cannot be told apart from a project-local value in the merged output. Add the item again after the migration.--yesauthorizes the installed-state loss a repair causes. Showcapshelf status <item> --diffand get permission before you pass it.
Refs accept a bare kind ref (skills/hello) or a scope-qualified one
(local/skills/hello, project/skills/hello).
The upgrade is one-way: an older binary refuses a version-4 lock outright. Upgrade capshelf on every machine and in CI first, then commit the migrated project lock as a lock-only change.
Plugin marketplaces
Use capshelf marketplace when the user wants to group canonical data-repo
skills into a Claude/Cowork or Codex plugin. Claude and Codex are independent
targets: always pass --target for mutations and do not mirror membership
unless the user asks.
Marketplace/plugin identities are kebab-case and plugin create requires at
least one canonical skill. Codex installation policy values are
NOT_AVAILABLE, AVAILABLE, and INSTALLED_BY_DEFAULT; authentication
policy values are ON_INSTALL and ON_USE. Target-inapplicable options are
errors. Validation JSON contains target-labeled configuration, projection,
source-path and file/byte accounting, known Cowork limits, and structured issue
objects. An explicit --cowork-url is a user assertion and produces a warning;
strict validation therefore refuses it unless the support can be classified
from the repository origin.
Run marketplace validate before publication. After direct Codex definition
or selected-skill edits, run marketplace sync --target codex --dry-run --json, review source and generated diffs together, then sync and commit them
together. Sync never stages or commits. If it refuses because an affected
projection path is dirty, surface every path and get permission before using
--yes. Marketplace mutations do make one local data-repo commit but never push.
For local handoff, marketplace plugin pack <name> --target claude --output <outside-path>.plugin builds a Cowork upload, while the data repo itself is
the primary local Codex marketplace. Stop after printing the runtime handoff:
do not register a marketplace, upload/install/refresh a plugin, restart an
app, or edit a runtime cache unless the user separately asks for that runtime
action.
Canonical skills have no Capshelf data-repo rename/delete command. If the user renames one directly, update every Claude and Codex membership in the same Git change, sync Codex, and validate both targets. Remove every membership before deleting a skill; dangling refs intentionally block validation, sync, packaging, and further marketplace mutations.
Proposing changes upstream (review required, or no direct push access)
Capshelf never pushes and never creates branches — branch in the data repo with ordinary git, let promote commit on the branch, then push and open a PR with gh:
DATA=$(capshelf data-path) # fallback: jq -r .dataRepo .capshelf/local.json
BRANCH=$(git -C "$DATA" symbolic-ref --short refs/remotes/origin/HEAD | sed 's|^origin/||') # the repo's default branch
capshelf sync-data
git -C "$DATA" switch -c propose/<topic> "origin/$BRANCH"
# edit the installed item in the project, then:
capshelf promote <item> -m "why"
git -C "$DATA" push -u origin propose/<topic>
gh pr create --repo <owner/data-repo> --head propose/<topic> --title "..." --body "..."
After the PR merges, re-pin the lock to the merged history (until then the lock pins the proposal-branch commit, which squash/rebase merges orphan):
git -C "$DATA" switch "$BRANCH"
capshelf sync-data
capshelf update <item>
Fork variant (read-only consumers): gh repo fork <owner/data-repo> --clone=false, git -C "$DATA" remote add fork <fork-url>, branch and promote as above, then git -C "$DATA" push -u fork propose/<topic> and gh pr create --repo <owner/data-repo> --head <user>:propose/<topic>. Capshelf's upstream verification only checks origin, so the extra fork remote is safe and sync-data keeps pulling from origin.
Pi extensions
pi-extensions/<name> maps pi/extensions/<name>/index.ts in the data repo to .pi/extensions/<name>/index.ts in the project. Extensions support the same project/clone-local lifecycle as skills, including add --local, share --to local, move --to local, and keep-local. Both Capshelf scopes materialize to Pi's project-local extension path; Capshelf does not manage user-global extensions.
Pi loads project extensions only after project trust, but then they execute arbitrary TypeScript with full system permissions. Always inspect source before add, before promote, and before asking the user to /reload or restart Pi. Capshelf does not sandbox code, validate TypeScript, edit .pi/settings.json, manage Pi packages, invoke package managers, or install package.json.dependencies; dependency declarations produce an advisory warning only. Do not run install commands on the user's behalf as part of capshelf reconciliation.
Subagents
capshelf add subagents/<name> installs every available Claude/Codex target
under one lock. Use show --target or get-path --target; add never accepts a
partial target. share subagents/<name> --to project adopts every matching
unmanaged project runtime file, while --from requires --target.
Subagents are project-scope only. Do not use --local, keep-local, or
promote --merge; the supported stale overwrite escape hatch is
promote --stale-ok with explicit user direction. Review subagents like
privileged runtime policy because they can combine instructions with tools,
models, permissions, MCP servers, and sandbox controls.
A subagent may carry a Claude source, a Codex source, or both — read the
targets: block before reporting which runtimes can use it. See Target
coverage.
Target coverage (mcp and subagents)
mcp/<name> and subagents/<name> each have two candidate runtime targets, so
an item covers Claude, Codex, or both. add, show, share, and status
report which. Read that report before telling the user an MCP server or
subagent is available: .mcp.json is Claude Code's project MCP file, so writing
it says nothing about Codex.
add, show, and share print the block whether or not there is a gap:
targets:
Claude written /abs/path/.claude/agents/claude-only.md
Codex absent no codex source in this item
Codex reads subagents/claude-only/codex.toml in your data repo.
Once it is committed there: capshelf update subagents/claude-only
status is a whole-project overview, so it stays silent on full coverage and
prints one sub-line per item that has a gap:
✓ data/mcp/github 2b7041f66a52 up-to-date
targets: Claude ✓ Codex ✗ — no codex source at the locked commit
Codex reads mcp/github/codex.toml; once it is committed there: capshelf update mcp/github
- A gap is a fact, not a fault. capshelf does not know which harnesses the
project uses, so a one-target item is a valid install: exit stays 0, there is
no
⚠glyph, and--strictis unaffected. Report the gap; never route around it by editing a generated output or re-runningadd. - Closing a gap is a data-repo change, then a per-project update. Author the
canonical source the message names, commit it in the data repo, then run
capshelf update <item>in each project that wants it.updateprints no coverage block, so confirm withcapshelf status <item>. targets: unknown (<reason>)is not a gap. Coverage is read at the locked commit, so an unbound data repo or an unreachable commit means capshelf can say nothing about coverage. Resolve that first (seemissing_source_commit), then re-read; do not report an unknown as a missing target.show --target <t>for an absent target exits 3 and prints the same guidance. That is the answer to "does this item support Codex?", not a failure to work around.settings/<name>andcodex-config/<name>have one candidate target each and print no block.add bundles/<name>reports no per-member coverage — its members' gaps appear instatus.rmreports every output it reconciled, so a two-target item names both.--jsonaddstargetCoveragetoadd,show,share, and eachstatusrow: one entry per candidate withpresent: true | false | null, pluscoverageState: "unknown"when null. Filter onpresentinstead of readingsources, which stays present-only.
Config fragments
Shared fragments merge into project config outputs: settings/<name>/settings.json → .claude/settings.json; mcp/<name>/claude.json → .mcp.json; mcp/<name>/codex.toml and codex/config/<name>/config.toml → .codex/config.toml. Outputs preserve unmanaged project-local values; capshelf refuses unmanaged scalar/shape collisions, and also refuses two fragments that set the same key to conflicting scalar values (naming both) rather than silently letting manifest order decide. JSON outputs are read as JSONC (comments tolerated) but rewritten as plain JSON, and TOML is reserialized. Capshelf detects comment loss during preflight; review the named output and get permission before using --yes.
Edit canonical source paths (from get-path), never the generated outputs, then capshelf promote <fragment> -m "message". share for fragments always lands in project scope (--to project is the default). To share an existing MCP server, capshelf share mcp/<server> with no flags is the common case: the pick defaults to the item name and capshelf adopts the server from every output that contains it unmanaged (.mcp.json and/or .codex/config.toml), in one commit. Other cases use:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 20
- Forks
- 1
- Last commit
- Sep 2026
ahel review
K1binfo
installs-packages
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
capshelf- Source
- github.com/genged/capshelf