capshelf

SkillAI & models

Use 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.

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:

  1. capshelf get-path <item> for the absolute editable path (fragments return canonical data-repo source files; --output returns generated outputs for inspection only).
  2. Edit with your Edit/Write tools.
  3. capshelf status <item> — should report drifted_local (or source_dirty for fragments).
  4. Decide with the user:
    • capshelf promote <item> -m "why" — push to the data repo. Other projects see update available next 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 recorded sourceCommit; first show capshelf status <item> --diff and get permission before using --yes in 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 under skills/, pi/extensions/, subagents/, settings/, mcp/, and codex/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 runs capshelf 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.md installs to .claude/agents/<name>.md; subagents/<name>/codex.toml installs 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.yml in 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 by init, read-only from a project's perspective.
  • data (lock prefix data/): live in your data repo. Added via add, removed via rm, adopted via share, pushed back via promote.

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).

verbpurpose
initinitialize 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 pathinspect or change the explicit data-repo binding (old set-data/set-upstream/data-path still work as aliases)
ls / show / search / statusinspect and discover (all support --json; ls and status include user-level runtime skills by default, --user narrows to them only)
add / rm / apply / update / revertconverge the project on its locks
lock migrateone-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-localflow 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-pathprint the editable path for an item; use `--target claude
self-updateupdate 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.
  • --yes authorizes the installed-state loss a repair causes. Show capshelf status <item> --diff and 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 --strict is unaffected. Report the gap; never route around it by editing a generated output or re-running add.
  • 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. update prints no coverage block, so confirm with capshelf 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 (see missing_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> and codex-config/<name> have one candidate target each and print no block. add bundles/<name> reports no per-member coverage — its members' gaps appear in status.
  • rm reports every output it reconciled, so a two-target item names both.
  • --json adds targetCoverage to add, show, share, and each status row: one entry per candidate with present: true | false | null, plus coverageState: "unknown" when null. Filter on present instead of reading sources, 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