RFC 0025: Agent-CLI Plugin Bundles
June 29, 2026 · View on GitHub
Status: accepted (V1)
Date: 2026-05-09
Context:
docs/SPEC.md § "Adapter Boundary",
docs/DECISION_LOG.md (D006, D009, D020, D028),
docs/rfcs/0010-tool-harness-profiles.md,
docs/rfcs/0012-local-service-api.md,
docs/rfcs/0015-self-contained-agent-skills.md,
src/striatum/skills/,
Claude Code plugins,
Claude Code plugins reference,
Codex CLI plugins,
Codex CLI build plugins,
Gemini CLI extensions,
Gemini CLI extension reference
Implemented in dogfood-028 (V1 step 1: claude_code plugin profile) and
dogfood-029 (V1 steps 2+3: codex and gemini plugin profiles).
Problem
RFC 0015 ships per-tool skills for Claude Code, Codex (custom-agent
roles), Gemini (best-effort generic), and a generic Markdown guide.
Skills are loose Markdown files at conventional paths
(.claude/skills/, .codex/agents/, etc.) and are sufficient for an
operator who is willing to (a) regenerate them via
striatum skills install after every Striatum upgrade and (b) drive the
agent CLI by hand.
Since RFC 0015 V1 landed, all three target agent CLIs have promoted skills into a higher-level packaging unit:
- Claude Code ships a plugin format (
.claude-plugin/plugin.json) that bundles skills, slash commands, sub-agents, hooks, MCP servers, LSP servers, and monitors under one manifest, namespaced by plugin name (e.g./striatum:claim-next), with marketplace-driven install via/plugin install <name>@<marketplace>. - Codex (≥ v0.117.0) ships a near-identical plugin format
(
.codex-plugin/plugin.json) that bundles skills, MCP servers, app connectors, and hooks, installable from local or git marketplaces viacodex plugin marketplace addplus amarketplace.jsondirectory. - Gemini CLI ships extensions (
gemini-extension.jsonat the extension root) that bundle aGEMINI.mdcontext file, custom commands undercommands/, MCP servers, hooks, skills, and sub-agents. Installed viagemini extensions install <source>.
Loose skill files do not benefit from any of this. They cannot ship
with their slash-command counterparts; they cannot ship a Striatum MCP
server entry alongside the prose; they cannot be installed once into
~/.claude/plugins/ and reused across many target repos by name; and
they offer no version handshake when an operator's installed Striatum
moves and the on-disk skills go stale.
This RFC defines how Striatum emits a plugin bundle for each of the
three agent CLIs that already understand the concept, so an operator
with one striatum plugin install invocation gets a packaged,
namespaced, version-stamped bundle that the agent CLI can install
through its own native mechanism.
Goals
- After a single
striatum plugin install --profile <id>invocation, the target environment contains a plugin bundle in the on-disk layout the agent CLI expects, with a valid manifest and the same skill content RFC 0015 already generates. - One plugin per agent-CLI family covers: skills (carry-over from
RFC 0015), the small set of imperative slash commands an operator
uses by name (
claim-next,status,why,dashboard,doctor), and a hook stub the operator can wire up later. - Bundles are regenerable, idempotent, version-stamped, and refuse to
clobber operator edits without
--force(same protocol asskills install). - The bundle stays self-contained per RFC 0015 D020: no external URLs, no source-repo dependency, no hosted services. Generation happens offline from packaged templates.
- Profiles
claude_code,codex,geminiship at parity in V1.genericcontinues to mean "single Markdown guide; no plugin packaging" and is unchanged from RFC 0015. - A local marketplace fixture is emitted alongside the bundle so the operator can register it via the agent CLI's own marketplace mechanism (Claude Code, Codex) without standing up a git repository or hosted service.
Non-Goals
- A hosted Striatum plugin marketplace, version registry, or update-fetch service. D020 stands; bundles ship inside the installed Python distribution.
- Auto-installing plugins into the agent CLI's user-scope directory on the operator's behalf. The runner emits files; the operator runs the agent CLI's install command.
- A Striatum MCP server. RFC 0010 V2 deferred MCP-based supervision
and per-packet skill installation; this RFC is silent on those.
The plugin manifest's
mcpServersentry, where present, ships empty in V1 and is reserved for a future RFC that lands a Striatum MCP server. - Generating workflow-specific task prompts. Task prompts live on the workflow's jobs (RFC 0015 § Non-Goals).
- Replacing
skills install. The two verbs coexist; the plugin surface is a strict superset for the three CLIs that support it and a no-op forgeneric. - Native parity for agent CLIs that lack a plugin format
(the historical
genericprofile). Those continue to be served byskills install --profile generic.
Proposal
1. New CLI verb: striatum plugin
striatum plugin install
[--target <path>] # default: target repo root
[--profile <id>] # claude_code | codex | gemini | all (default: claude_code)
[--scope project|user] # default: project; user writes under ~/
[--namespace <prefix>] # default: striatum
[--force] # overwrite operator-edited files
[--dry-run] # print plan, write nothing
[--with-marketplace] # also emit marketplace.json fixture (default on for claude_code, codex)
[--json]
striatum plugin uninstall --profile <id> removes a previously
generated bundle's tracked files (manifest-driven; same edit-detect
protocol as --force).
The same packaged-template mechanism that powers RFC 0015 powers
this verb. Templates live at
src/striatum/plugins/templates/<profile>/... and reuse the
existing src/striatum/skills/templates/<profile>/ skill bodies
verbatim — the plugin layer is a packaging change, not a content
rewrite.
2. Per-profile bundle layout
V1 ships three first-class plugin profiles. Paths are written
relative to --target (the target repo) when --scope project,
or under ~/ when --scope user.
2.1 claude_code
Project scope writes under .striatum/plugins/claude_code/:
.striatum/plugins/claude_code/
├── .claude-plugin/
│ └── plugin.json
├── skills/
│ ├── striatum-workflow/SKILL.md
│ ├── striatum-scaffold/SKILL.md
│ ├── striatum-claim-loop/SKILL.md
│ ├── striatum-supervise/SKILL.md
│ └── striatum-recover/SKILL.md
├── commands/
│ ├── claim-next.md
│ ├── status.md
│ ├── why.md
│ ├── dashboard.md
│ └── doctor.md
├── hooks/
│ └── hooks.json # empty handlers + commented stubs
├── .mcp.json # empty {} placeholder (reserved)
├── .manifest.json # Striatum-side regeneration manifest
└── README.md # how to install via `/plugin install ./...`
.claude-plugin/plugin.json carries:
{
"name": "striatum",
"description": "Drive Striatum workflows from Claude Code: claim work, publish artifacts, recover stalled runs.",
"version": "0.7.3",
"author": {"name": "Striatum"},
"license": "MIT"
}
User scope (--scope user) writes under
~/.claude/plugins/striatum/ instead. After install, the operator
runs claude --plugin-dir .striatum/plugins/claude_code (project)
or /plugin install ./.striatum/plugins/claude_code (project,
session-scoped) to load the bundle. --with-marketplace also
emits a sibling .striatum/plugins/marketplace.json so the
operator can /plugin marketplace add ./.striatum/plugins/ and
install via /plugin install striatum@local-striatum.
The five skills are byte-identical to RFC 0015's
claude_code profile output. Skill files in a plugin bundle use
namespaced invocation (/striatum:claim-loop), which the bundle's
own commands/*.md slash commands are also reachable through
(e.g. /striatum:claim-next).
2.2 codex
Project scope writes under .striatum/plugins/codex/:
.striatum/plugins/codex/
├── .codex-plugin/
│ └── plugin.json
├── skills/
│ ├── striatum-workflow/SKILL.md
│ └── ... (same five as claude_code)
├── hooks/
│ └── hooks.json
├── .mcp.json # empty {} placeholder
├── .manifest.json
└── README.md
.codex-plugin/plugin.json:
{
"name": "striatum",
"version": "0.7.3",
"description": "Drive Striatum workflows from Codex.",
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"hooks": "./hooks/hooks.json",
"interface": {
"displayName": "Striatum",
"shortDescription": "Local-first workflow runner for terminal AI agents.",
"category": "Developer Tools"
}
}
--with-marketplace emits .striatum/plugins/marketplace.json with
a {"source": "local", "path": "./codex"} entry. The operator runs
codex plugin marketplace add ./.striatum/plugins/ and then
@striatum from inside Codex.
2.3 gemini
Project scope writes under .striatum/plugins/gemini/:
.striatum/plugins/gemini/
├── gemini-extension.json
├── GEMINI.md # context: top-level Striatum-driving guide
├── commands/
│ ├── claim-next.toml
│ ├── status.toml
│ ├── why.toml
│ ├── dashboard.toml
│ └── doctor.toml
├── skills/ # carry-over from RFC 0015 generic
│ └── striatum-workflow/SKILL.md ... (five files)
├── agents/
│ └── striatum-recover.md
├── .manifest.json
└── README.md
gemini-extension.json:
{
"name": "striatum",
"version": "0.7.3",
"description": "Drive Striatum workflows from Gemini CLI.",
"contextFileName": "GEMINI.md",
"excludeTools": []
}
User scope writes under ~/.gemini/extensions/striatum/. From
project scope the operator runs
gemini extensions install ./.striatum/plugins/gemini to copy the
bundle into ~/.gemini/extensions/striatum/. Gemini's extension
mechanism does not have a marketplace concept comparable to the
other two, so --with-marketplace is a no-op for this profile and
prints a notice instead of writing a file.
This is also the first time gemini is a first-class profile;
RFC 0015 V1 fell back to generic. The skill bodies that were
already generated under src/striatum/skills/templates/gemini/
(currently a single STRIATUM_GEMINI_GUIDE.md.tmpl) are split into
the same five-skill shape Claude Code uses, sharing the underlying
content. The Markdown guide remains available under
skills install --profile generic for tools without an extension
format.
3. Generation manifest
Each bundle root contains .manifest.json (separate from the
agent-CLI's own manifest):
{
"schema_version": "striatum.plugins.manifest.v1",
"striatum_version": "0.7.3",
"generated_at": "2026-05-09T18:42:11Z",
"profile": "claude_code",
"namespace": "striatum",
"files": [
{
"path": ".claude-plugin/plugin.json",
"sha256": "ab12...",
"template": "claude_code/plugin.json.tmpl"
},
{
"path": "skills/striatum-claim-loop/SKILL.md",
"sha256": "cd34...",
"template": "claude_code/skills/striatum-claim-loop.md.tmpl"
}
]
}
The manifest is the single source of truth for "did the operator
edit this file?". It is not consulted by the runner at workflow
time; it is read only by plugin install and doctor. Same
contract as RFC 0015's .manifest.json for skills.
4. Marketplace fixture
For claude_code and codex, --with-marketplace (default on) also
writes .striatum/plugins/marketplace.json:
{
"name": "local-striatum",
"interface": {"displayName": "Local Striatum"},
"plugins": [
{
"name": "striatum",
"source": {"source": "local", "path": "./claude_code"},
"policy": {"installation": "AVAILABLE"},
"category": "Developer Tools"
},
{
"name": "striatum",
"source": {"source": "local", "path": "./codex"},
"policy": {"installation": "AVAILABLE"},
"category": "Developer Tools"
}
]
}
A marketplace.json that already exists at the same path is treated
under the same edit-detect / --force rules as any other generated
file. Profiles whose marketplace entry isn't present in the file
are appended; profiles already listed are updated in place when
their version moves.
5. Slash command set
V1 ships exactly five slash commands per profile, each a thin imperative wrapper around the CLI. They exist so operators don't have to type the full Striatum verb in chat:
| Command | Wraps | Notes |
|---|---|---|
claim-next | striatum claim-next --session-id $S --json | Reads session_id from env or prompts. |
status | striatum status --run-id $RUN --json | |
why | striatum why --run-id $RUN --job-id $JOB | |
dashboard | striatum dashboard --run-id $RUN --once | Single-frame mode for chat. |
doctor | striatum doctor --verbose --json |
Slash-command files use each CLI's native format: Markdown for
Claude Code, Markdown for Codex (sharing the same body shape), and
TOML for Gemini CLI. Bodies are templated from the parser, exactly
as skills install already does.
6. Hook stub
V1 emits an empty hooks/hooks.json (or hooks.json for Gemini)
with commented stubs the operator can opt into. No hooks fire by
default. Two stubs are shipped commented out:
{
"hooks": {
"Stop": [
// Example: auto-call `striatum complete` when the agent finishes.
// Uncomment and adjust to fit your workflow.
// {"matcher": "*", "hooks": [{"type": "command", "command": "striatum complete --session-id $SESSION_ID --job-id $JOB_ID"}]}
],
"PreToolUse": [
// Example: gate file writes against the work packet's write_scope.
// {"matcher": "Write|Edit", "hooks": [{"type": "command", "command": "striatum check-write-scope --path $TOOL_PATH"}]}
]
}
}
The runner does not interpret these hooks; they exist so
plugin install is the obvious place an operator adds them, and so
operator edits to hooks.json are protected by the manifest
edit-detect rule.
7. striatum init --with-plugins and doctor checks
striatum initgains--with-plugins [profile]. When passed, the same code path asplugin installruns immediately after.striatum/is created. Mirrors RFC 0015's--with-skills.--with-skillscontinues to work and is independent.striatum doctoradds two checks:plugin_missing— the manifest says a bundle should exist for a profile that operators are using, but the bundle directory is gone.plugin_outdated— the manifest'sstriatum_versionis older thanstriatum.__version__, or templates' hashes differ from the manifest. Both surface the exactstriatum plugin installinvocation that fixes them. Doctor never auto-regenerates.
8. Self-contained guarantee
The same three guarantees from RFC 0015 § Self-contained extend to plugin bundles:
- No external links. Generation refuses to emit a URL outside the bundle's namespace directory. Cross-skill references use relative paths inside the bundle.
- No source-repo dependency. Templates ship inside the installed Python package.
- Self-describing version. Each bundled file's header records the runner version. If a bundle and the installed runner disagree, the operator is told to regenerate.
Acceptance Criteria
striatum plugin install --profile claude_codein an empty target writes the directory tree above (manifest +.claude-plugin/plugin.json+ 5 skills + 5 commands +hooks/hooks.json+.mcp.json+README.md), and a second invocation with the same install is byte-identical.striatum plugin install --profile codexwrites the parallel.codex-plugin/-rooted bundle.striatum plugin install --profile geminiwritesgemini-extension.json+GEMINI.md+commands/(TOML) +skills/(five SKILL.md) +agents/. Promotesgeminito a first-class profile (closes the RFC 0015 V1 fallback).striatum plugin install --profile allwrites all three bundles plus a single sharedmarketplace.json.- After install, an operator-modified
commands/claim-next.mdis preserved across a re-run;--forceoverwrites it;--dry-runprints a plan that names the conflict. striatum init --with-pluginsproduces a target tree containing.striatum/operational scratch plus the requested plugin bundle; it does not create live workflow state inretired-local-statefor current production runs.striatum doctorreportsplugin_missingfor a target whose manifest references a bundle that has been deleted, andplugin_outdatedafter the package is upgraded butplugin installhas not been re-run.- A Claude Code session pointed at a fresh clone of an unrelated
target repo, after
striatum init --with-plugins claude_code, can run/plugin marketplace add ./.striatum/plugins/,/plugin install striatum@local-striatum, and then drive a packet through the claim/ack/publish/complete loop using only the bundle. - The Codex profile produces a bundle that survives
codex plugin marketplace add ./.striatum/plugins/followed by install via the Codex marketplace browser. - The Gemini profile produces a bundle installable via
gemini extensions install ./.striatum/plugins/gemini. - Generated bundle content contains no URLs and no paths outside the bundle root. A unit test enforces this on rendered output (mirrors RFC 0015's no-external-URL invariant).
tests/test_plugin_install.pycovers profile selection, idempotent regeneration, edit-detection refusal,--force,--dry-run, manifest shape, marketplace.json append/update semantics, and the no-external-URL invariant.
Open Questions
- Plugin vs. skill double-install. A target that runs both
skills installandplugin installfor the same profile ends up with two copies of the same skill content (once in.claude/skills/, once in.striatum/plugins/claude_code/skills/). V1 leaves them independent; doctor surfaces both manifests. Open question: shouldplugin install --profile claude_codeimply removing the loose.claude/skills/striatum-*/, or stay silent? Default-silent is safest; reviewer pushback welcome. - MCP server entry shape. The
.mcp.jsonplaceholder is empty in V1 because Striatum has no MCP server today. RFC 0010 V2 deferred this. When that future RFC lands, the manifest'smcpServersentry becomes load-bearing — should this RFC pre-reserve a stable shape (e.g.{"striatum": {"command": "striatum", "args": ["mcp", "serve"]}}) or wait for the MCP RFC to define it? V1 reserves the file path but ships an empty object so adding the server later is additive. - Slash command surface size. V1 ships five commands. A
larger surface (
requeue-stale,submit-review,publish-artifact,worktree-create,decision) would cover more of the agent's actual flow but bloats the install. V1 picks the five that show up most often in operator chat transcripts; promotion is additive. - Marketplace name collisions. The marketplace fixture is
named
local-striatum. If two target repos both register their fixtures into the same Claude Code user scope, the secondmarketplace addeither overwrites or errors depending on the agent CLI's policy. V1 documents the collision; an opt-in--marketplace-nameflag is a follow-up. - Codex
apps/andassets/. The Codex manifest accepts anappsfield (app connectors) andassets/(icons, brand imagery). V1 omits both — Striatum has no hosted-service connector and no brand assets to ship. Operators can add them by hand; the manifest's edit-detect protects their additions. Promoting to first-class is a follow-up. - Gemini extensions install flow with a directory prefix. The
Gemini docs say
gemini extensions install <source>where<source>is a path or git URL. V1 documents the path form. Whether bundling a per-target git repo is preferable to the copy-into-~/.gemini/extensions/flow is operator preference; V1 stays with copy. - Bundling RFC 0015's
genericprofile.genericdoes not map to any plugin format. V1 leaves it underskills install. An open question: shouldplugin install --profile allskipgenericsilently (current proposal) or emit a single Markdown bundle alongside the three plugins? V1 skips silently. - User-scope vs project-scope default. Mirrors RFC 0015's
open question. Project scope keeps the bundle reproducible
across machines and survives
git clean-vs-.striatum/cleanup; user scope means one install across many target repos. V1 defaults to project; user scope is--scope user.
Domain Modeling
The plugin bundle is a value object projected from the
existing Plugin Profile aggregate root introduced by RFC 0010 and
RFC 0015. It has no identity of its own; two bundles produced by
the same Striatum version, profile, and namespace are
interchangeable. The aggregate root remains the runner-installed
template set; the manifest is the bundle's projection key. See
docs/DDD.md § "Adding to the model".
The closed set of supported plugin profiles
(claude_code, codex, gemini) is a value object; promoting a
new profile is a workflow decision, not a runtime decision.
Relationship To Other RFCs
- RFC 0015 — self-contained agent skills. This RFC layers
packaging on top. Skill bodies are reused; the new code path
produces a plugin manifest, slash commands, and a hooks stub
alongside them. RFC 0015's
skills install, manifest contract, edit-detection, and self-contained guarantees are inherited verbatim. - RFC 0010 — tool harness profiles. Profile fields decide
which agent-CLI idioms appear inside the plugin (slash command
format, hook event names, MCP entry shape). Promoting
geminihere closes RFC 0015 V1's open item on Gemini parity. - RFC 0012 — local service API. Independent of this RFC. A
future Striatum-as-MCP-server (deferred under RFC 0010 V2)
would populate the
mcpServersentry that V1 ships empty. - D006 / D009, superseded for current substrate/interface behavior by D094 / D104 — daemon-owned PostgreSQL is the live state, and agents update state through approved daemon clients. Plugin slash commands wrap the CLI; they do not touch live state directly. The bundle's hooks stub, when an operator opts in, also calls the CLI.
- D020 — no hosted services. Plugin bundles ship inside the
installed Python distribution. Generation is offline. The
marketplace fixture is
local-source by construction; no git remote, no hosted endpoint. - D028 — no transcripts. Plugin slash commands and hooks do not capture or emit model output as authoritative state.
Implementation Path
V1 ships in three landable steps. Each step has its own acceptance test set; RFC 0025 is "accepted" once steps 1 and 2 land, with step 3 promoting additional profiles without re-opening the RFC.
- Generator core + Claude Code plugin profile. Add
src/striatum/plugins/with templates and a renderer that wrapssrc/striatum/skills/templates/claude_code/. Add thestriatum plugin installverb. Ship the Claude Code bundle plus marketplace fixture. Addtests/test_plugin_install.py. Smallest tractable PR; gives operators a working bundle on the most-exercised CLI. - Codex plugin profile + bootstrap convenience. Add the
Codex
.codex-plugin/profile, the--with-pluginsflag onstriatum init, and theplugin_missing/plugin_outdateddoctor checks. - Gemini extension profile. Add the Gemini
gemini-extension.jsonprofile with split-out skill content (was generic in RFC 0015 V1). Promotegeminito first-class across docs.