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>/mcp for 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 skills path (Soma bundles no mcpServers and no hooks)
  • defines userConfig settings 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:

HookTriggerCommand
SessionStartevery Claude Code session start<binary> setup plugin-hook
ConfigChangeplugin 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, and logo

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:

FieldPurpose
interface.displayNamehuman-readable plugin name
interface.shortDescriptionshort listing text
interface.longDescriptionfull listing text
interface.capabilities["Read"] or ["Read", "Write"]
interface.defaultPromptthree realistic prompts
interface.brandColorservice-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 version field
  • 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-hook directly
  • every skill has name: and description: 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 check first
  • run setup repair only when needed and only when --no-repair is absent
  • emit the setup report as structured JSON
  • include exit_policy, blocking_failures, advisory_failures, ran_repair, and no_repair
  • exit 0 for 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:

FileFields
Cargo.tomlpackage version, homepage/repository when present
plugins/soma/.claude-plugin/plugin.jsonidentity, repository, user config; no version field
plugins/soma/.codex-plugin/plugin.jsonidentity, repository, interface metadata; no version field
plugins/soma/gemini-extension.jsonidentity, repository, settings
server.jsonpackage 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:

  1. Rename example, Example, and EXAMPLE across plugin files.
  2. Update all three manifests with the real repository, description, author, keywords, and capability claims.
  3. Keep credential names aligned across Claude userConfig, Codex plugin settings, and Gemini settings.
  4. Replace upstream credential fields such as soma_api_url and soma_api_key.
  5. Update apply_plugin_options() in crates/soma/cli/src/setup.rs to map service-specific plugin options into env vars.
  6. Implement <binary> setup plugin-hook, --no-repair, check, and repair.
  7. Update shared skill docs for the actual action surface.
  8. Replace Codex defaultPrompt entries with realistic prompts.
  9. Update Gemini description, settings, and contextFileName if needed.
  10. Run just validate-plugin, plugin contract tests, and scripts/check-plugin-hook-contract.py before 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-hook directly (Soma itself ships no hooks — its test asserts no manifest declares a hooks key and no hooks/hooks.json exists)
  • apply_plugin_options() maps CLAUDE_PLUGIN_OPTION_* into the binary's env vars
  • setup plugin-hook --no-repair parses and does not mutate appdata
  • JSON plugin-hook output contains exit_policy, blocking_failures, advisory_failures, ran_repair, and no_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