Plugin Surfaces
August 3, 2026 · View on GitHub
Soma ships one service plugin package with three host-specific entrypoints:
- Claude Code:
plugins/soma/.claude-plugin/plugin.json - Codex:
plugins/soma/.codex-plugin/plugin.json - Gemini:
plugins/soma/gemini-extension.json
All three surfaces should describe the same MCP server and expose the same skills. Upstream-client servers should prefer a local stdio binary install for plugin use. Application/platform servers may point plugins at the shared HTTP MCP endpoint when a central server deployment is the source of truth. The host manifests differ, but the service behavior should not.
The shared descriptor is plugins/soma/plugin.surface.json. Run
cargo xtask generate-docs after editing it; the Claude, Codex, and Gemini
manifests are generated from that descriptor plus ENV_KEY_SPECS.
The local plugin default is stdio-first. See
docs/adr/0001-stdio-first-plugin-adapter.md
for the accepted decision and
docs/contracts/plugin-stdio-adapter.md
for the exact manifest and adapter contract.
Layout
plugins/soma/
.claude-plugin/
plugin.json # Claude Code manifest
.codex-plugin/
plugin.json # Codex manifest
README.md # Codex manifest field reference
gemini-extension.json # Gemini CLI extension manifest
hooks/
hooks.json # Claude lifecycle hook declarations (call the binary directly)
skills/
example/
SKILL.md # Shared action documentation
scaffold-project/
SKILL.md # Approval-first Soma adaptation handoff skill
When adapting Soma, rename example, Example, and EXAMPLE consistently across the package, then update host-specific display text and credentials.
Shared Contract
Each plugin surface should agree on:
- service name and repository URL
- MCP server name
- MCP connection profile: stdio command for local/plugin installs, or HTTP URL shape
<server_url>/mcpfor explicit remote/gateway deployments - bearer token setting name
- upstream service credential names
- action list and skill documentation
- read/write capability claims
Keep the plugin manifests thin. Runtime setup belongs in the service binary, not in manifest-specific shell code.
Soma's canonical repository is https://github.com/dinglebear-ai/soma, and
its OCI package is ghcr.io/dinglebear-ai/soma. Keep those identities aligned
across every plugin and marketplace manifest.
Claude Code
Claude Code uses plugins/soma/.claude-plugin/plugin.json.
Responsibilities:
- identifies the plugin and repository
- declares the
skillspath (Soma bundles nomcpServersand nohooks) - defines
userConfigsettings exposed in Claude Code - marks sensitive values with
sensitive: true
userConfig here is declarative, not auto-applied. Soma deliberately ships
no .mcp.json — the server is expected to be reached through the operator's
existing gateway or local MCP setup — so there is no env block for
${user_config.*} to flow into, and no lifecycle hook to bridge them either.
The fields declare what an operator must wire into their own gateway config;
filling them in the Claude Code settings UI configures nothing by itself.
apps/soma/tests/plugin_contract.rs asserts the core five are present.
Every server generated from this scaffold does the opposite: it ships an
.mcp.json whose env block maps each userConfig key to a <SERVICE>_*
variable, which is what actually delivers config to a stdio server. If you are
deriving a server, wire that env block — do not inherit Soma's
gateway-oriented shape by accident.
Monitors
plugins/soma/monitors/monitors.json commands must not reference
${user_config.*}: Claude Code v2.1.207+ rejects that in monitor commands and
the monitor then silently never starts. Monitor processes also receive no
CLAUDE_PLUGIN_OPTION_* environment variables. soma watch therefore takes no
--url and resolves its own default from config; a monitor needing a
non-default URL must read it from a config file inside the script. Do not
hardcode URLs in monitors.json either.
auth_mode=oauth enables whichever OIDC providers are configured via env vars
(Google, Authelia, GitHub — see docs/AUTH.md), not only Google;
auth_admin_email bootstraps an allowed email shared across all configured
providers. Keep the auth_mode/auth_admin_email setting descriptions in the
Claude, Codex, and Gemini manifests in sync — they are generated by
cargo xtask generate-docs from scripts/generate-docs.py.
Lifecycle hooks
Soma ships none. No manifest declares a hooks key and there is no
plugins/soma/hooks/hooks.json. Setup therefore does not run automatically;
invoke soma setup plugin-hook yourself after install or a settings change.
The rest of this section documents the cross-repo hook standard that the
other Rust MCP servers (cortex, gotify, unifi, tailscale, apprise, unraid) do
implement, and which cargo xtask check-plugin-hook-contract audits. A server
that wires hooks declares them at plugins/<service>/hooks/hooks.json with:
| Hook | Trigger | Command |
|---|---|---|
SessionStart | every Claude Code session start | <binary> setup plugin-hook |
ConfigChange | plugin user settings change | <binary> setup plugin-hook |
The hook calls the binary directly (no shell wrapper). The standard command is:
<binary> setup plugin-hook
For rollout audits, the binary must also support:
<binary> setup plugin-hook --no-repair
The hook command runs the binary already installed on PATH. It may map CLAUDE_PLUGIN_OPTION_* values into runtime env vars, create the appdata directory, and call the binary's setup logic. It should not own Docker/systemd orchestration, config rewriting, smoke-test policy, or failure classification.
Codex
Codex uses plugins/soma/.codex-plugin/plugin.json.
Responsibilities:
- identifies the plugin for Codex listings
- points at shared
skills - describes the interface shown in Codex UI
- declares read/write capabilities
- provides example prompts
- provides branding fields such as
brandColor,composerIcon, andlogo
Codex does not use Claude lifecycle hooks. Its manifest should still point to the same MCP server and shared skills so behavior stays aligned with Claude Code.
Codex-specific fields to adapt:
| Field | Purpose |
|---|---|
interface.displayName | human-readable plugin name |
interface.shortDescription | short listing text |
interface.longDescription | full listing text |
interface.capabilities | ["Read"] or ["Read", "Write"] |
interface.defaultPrompt | three realistic prompts |
interface.brandColor | service-appropriate hex color |
See plugins/soma/.codex-plugin/README.md for the full manifest field reference.
Gemini
Gemini uses plugins/soma/gemini-extension.json.
Responsibilities:
- identifies the extension
- declares Gemini settings
- launches the local stdio MCP adapter
- points at shared skills
- optionally points Gemini at a context file with
contextFileName
The Gemini manifest uses settings.* interpolation instead of Claude/Codex user_config.* interpolation:
"env": { "SOMA_API_URL": "${settings.soma_api_url}" }
Sensitive Gemini settings use:
"sensitive": true
Keep Gemini setting names aligned with Claude/Codex where possible. For example, prefer server_url, api_token, <service>_api_url, and <service>_api_key across all three surfaces.
Trusted gateway deployments may also expose trace_headers, mapped to
SOMA_MCP_TRACE_HEADERS. Its values are off, trusted, and
trusted-with-baggage; the runtime rejects trusted header modes unless the
server is using a loopback or trusted-gateway auth policy. Keep this option
aligned across generated Claude and Gemini settings when adapting the plugin.
The generated plugin option/env mapping table lives at
docs/generated/plugin-settings.md. It is
rendered from ENV_KEY_SPECS; update the registry first when adding plugin
settings or runtime env vars.
Shared skill action references are generated from the service-owned
ACTION_SPECS registry by cargo xtask generate-docs. When action metadata,
REST routes, or CLI/MCP visibility changes, regenerate docs and confirm the
shared skill text still matches the service registry. The generator expands
the modular Python environment, worker, generation, and graduation action
specs; their scope and confirmation requirements must stay identical across
all plugin hosts.
Plugin Validation
Run the plugin layout validator after changing manifests, MCP config, hooks, or skills:
just validate-plugin
# or
scripts/validate-plugin-layout.sh
The validator checks:
- Claude, Codex, and Gemini manifests are valid JSON
- plugin manifests do not contain a
versionfield - manifests point to hooks and skills paths
- documented stdio MCP registration launches
soma mcp - Gemini config launches
soma mcp - HTTP MCP remains available as a documented fallback for remote/gateway deployments
- hook config runs
<binary> setup plugin-hookdirectly - every skill has
name:anddescription:frontmatter
Use PLUGIN_ROOT=plugins/<service> when validating an adapted service package.
For release checks, just pre-release includes this validator and the other
Soma gates.
Stdio MCP Config
When registering Soma with an MCP client, use stdio mode through the installed binary:
{
"mcpServers": {
"example": {
"type": "stdio",
"command": "soma",
"args": ["mcp"],
"env": {
"SOMA_API_URL": "${user_config.soma_api_url}",
"SOMA_API_KEY": "${user_config.soma_api_key}",
"RUST_LOG": "warn"
}
}
}
}
Gemini carries equivalent MCP config directly in gemini-extension.json because its interpolation model is different.
Skills
plugins/soma/skills/soma/SKILL.md is shared across Claude, Codex, and Gemini. Every skill follows the three-tier fallback pattern — agents try each tier in order and stop when one works:
# soma — Claude Code Skill
Use this skill whenever you need to query or manage the Soma runtime.
## Tier 1: MCP tool (preferred)
Use when the Soma MCP server is configured in your agent.
soma(action="things")
soma(action="thing", id="abc123")
soma(action="help") # always available, no auth required
## Tier 2: CLI binary
Use when MCP is unavailable but the binary is installed in $PATH.
soma things [--json]
soma thing <id> [--json]
soma status
Env required: SOMA_API_URL, SOMA_API_KEY
## Tier 3: Direct API (last resort)
Use when neither MCP nor CLI is available.
curl -H "Authorization: Bearer $SOMA_API_KEY" \
"$SOMA_API_URL/things"
## Gotchas
- [service-specific pitfalls go here]
- [e.g. pagination, required headers, rate limits]
The skill should also include:
- quick action table (action → description → required params)
- full parameter reference with types
- common workflows (status check → list → inspect)
- response shapes for key actions
- sensitive-value handling notes (never log tokens, etc.)
Do not maintain separate skill docs per host. Update the shared skill when the action surface changes; Claude, Codex, and Gemini all read the same file.
Binary-Owned Hook Standard
Every Rust server with a Claude plugin should expose:
<binary> setup plugin-hook
<binary> setup plugin-hook --no-repair
<binary> setup check
<binary> setup repair
setup plugin-hook should:
- run
setup checkfirst - run
setup repaironly when needed and only when--no-repairis absent - emit the setup report as structured JSON
- include
exit_policy,blocking_failures,advisory_failures,ran_repair, andno_repair - exit
0for success or advisory failures - exit nonzero for blocking failures
- enforce a bounded total hook runtime
Advisory failures are non-blocking local conditions such as missing .env files when process env already supplies values, occupied MCP ports, optional startup proofs, or model prewarm. Blocking failures are prerequisites required for the plugin to function, such as missing appdata directories, missing required upstream credentials, or invalid OAuth/auth configuration.
Use scripts/check-plugin-hook-contract.py to audit the cross-repo standard:
# Static hook/delegation checks for all known Rust servers.
scripts/check-plugin-hook-contract.py
# Also run each binary's `setup plugin-hook --no-repair` JSON contract.
scripts/check-plugin-hook-contract.py --execute
Version And Release Sync
Keep version and metadata synchronized across:
| File | Fields |
|---|---|
Cargo.toml | package version, homepage/repository when present |
plugins/soma/.claude-plugin/plugin.json | identity, repository, user config; no version field |
plugins/soma/.codex-plugin/plugin.json | identity, repository, interface metadata; no version field |
plugins/soma/gemini-extension.json | identity, repository, settings |
server.json | package version and registry metadata, when present |
Release-please owns Soma version bumps in normal development. On release PR
branches, use cargo xtask sync-release-please-version to copy
.release-please-manifest.json into every derived file declared in
release/components.toml, then use cargo xtask check-version-sync or
just pre-release to verify that version-bearing files still agree. Plugin
manifests should remain versionless.
Soma should not claim write capability unless the MCP server has real write actions. Read-only servers should use Codex ["Read"] and avoid write-oriented sample prompts.
Adaptation Checklist
When creating a real server from Soma:
- Rename
example,Example, andEXAMPLEacross plugin files. - Update all three manifests with the real repository, description, author, keywords, and capability claims.
- Keep credential names aligned across Claude
userConfig, Codex plugin settings, and Geminisettings. - Replace upstream credential fields such as
soma_api_urlandsoma_api_key. - Update
apply_plugin_options()incrates/soma/cli/src/setup.rsto map service-specific plugin options into env vars. - Implement
<binary> setup plugin-hook,--no-repair,check, andrepair. - Update shared skill docs for the actual action surface.
- Replace Codex
defaultPromptentries with realistic prompts. - Update Gemini
description,settings, andcontextFileNameif needed. - Run
just validate-plugin, plugin contract tests, andscripts/check-plugin-hook-contract.pybefore release.
Required Tests
Each server should include tests that prove:
- Where a server does wire lifecycle hooks, its Claude hook config calls
<binary> setup plugin-hookdirectly (Soma itself ships no hooks — its test asserts no manifest declares ahookskey and nohooks/hooks.jsonexists) apply_plugin_options()mapsCLAUDE_PLUGIN_OPTION_*into the binary's env varssetup plugin-hook --no-repairparses and does not mutate appdata- JSON plugin-hook output contains
exit_policy,blocking_failures,advisory_failures,ran_repair, andno_repair - advisory failures exit
0 - blocking failures exit nonzero
- Claude, Codex, and Gemini manifests use the same service name, endpoint, token setting, and credential fields