atrium — Docs
docs/adapters/building.md

Building an adapter

The SDK v2 declarative manifest spec.

atrium uses SDK v2 adapters, which are largely declarative. Most integration (binary discovery, session discovery, launch composition, launcher UI) lives in adapter.json. Shell scripts are only needed for the one method that stays dynamic — hooks.

SDK v1 (fully script-based: detect_running.sh, extract_session_id.sh, list_recent_sessions.sh, build_resume_command.sh) is deprecated and no longer supported.

Manifest schema

Canonical schema: atrium-adapters/schemas/adapter.schema.json.

{
  "sdkVersion": 2,
  "name": "my-tool",
  "displayName": "My Tool",
  "description": "My AI coding assistant",
  "accent": "#4CAF50",
  "binary": "my-tool",
  "version": "1.0.0",
  "author": "me",
  "icon": "icon.svg",
  "skillInstallPath": "~/.my-tool/skills/atrium/SKILL.md",

  "binaryDiscovery": {
    "commands": ["my-tool"],
    "wellKnownPaths": [
      "~/.npm/bin/my-tool",
      "/usr/local/bin/my-tool",
      "/opt/homebrew/bin/my-tool",
      "~/.local/bin/my-tool"
    ]
  },

  "sessions": {
    "pattern": "~/.my-tool/history/*.json",
    "titleField": "title",
    "idField": "id"
  },

  "launch": {
    "base": ["my-tool"],
    "resumeFlag": ["--resume", "{session_id}"],
    "flagMap": {
      "thinking": ["--thinking"],
      "model": ["--model", "{value}"]
    }
  },

  "launcherOptions": [
    {
      "id": "thinking",
      "kind": "toggle",
      "label": "Thinking mode",
      "default": false
    },
    {
      "id": "model",
      "kind": "select",
      "label": "Model",
      "options": [
        { "label": "Fast", "value": "fast" },
        { "label": "Smart", "value": "smart" }
      ],
      "default": "smart"
    }
  ],

  "hooks": {
    "session-start": "atrium://hooks/my-tool/session-start",
    "session-end": "atrium://hooks/my-tool/session-end"
  },

  "methods": {
    "hooks": { "script": "hooks.sh" }
  }
}

Required fields

FieldTypeNotes
sdkVersionintegerMust be 2.
namekebab-case stringMachine ID. Must match the directory name.
displayNamestringShown in the launcher and Settings → Tools.
descriptionstringOne-liner.
accent#RRGGBBUI accent color for this adapter's panes.
binarystringName of the wrapped CLI tool.
versionsemverAdapter version, independent of the tool's version.
methodsobjectMaps method names to their implementation. In SDK v2 only hooks is typically needed.

Optional fields

FieldTypeNotes
iconfilenameInline brand SVG, loaded at adapter init. See icon.
skillInstallPathpath stringWhere to copy the canonical atrium skill. Tilde-expanded.
authorstring
binaryDiscoveryobjectHow atrium resolves the binary. See below.
sessionsobjectHow atrium lists recent sessions for the resume picker.
launchobjectHow atrium composes the launch command.
launcherOptionsarrayPer-launch UI inputs shown in the launcher.
hooksobjectEvent-name → atrium://hooks/<adapter>/<event> URI map.
chatTransportobjectEnables agent chat panes. See below.
updateCommandstringCommand that updates the harness binary itself.

minAppVersion

Adapters that depend on manifest fields newer than the oldest atrium version they target should set minAppVersion (semver) to gate themselves. atrium refuses to load an adapter whose minAppVersion exceeds the running app version, surfacing a clear error instead of failing schema validation cryptically. The icon field requires minAppVersion: 0.152.0.

chatTransport

Declaring a chatTransport is what lets your adapter run in an agent chat pane rather than only a terminal. Without it, the adapter is terminal-only — which is a perfectly valid thing to ship.

{
  "chatTransport": {
    "kind": "claude-sdk",
    "launchEnv": { "DISABLE_AUTOUPDATER": "1" },
    "capabilities": {
      "resume": true,
      "setModel": true,
      "thinking": true,
      "permissionCallback": true,
      "commandDiscovery": true,
      "planMode": true,
      "contextUsage": true,
      "imageInput": true,
      "checkpointing": true,
      "subagentEvents": true,
      "steering": true,
      "backgroundTasks": true
    }
  }
}

kind

KindFor
acpAny CLI speaking the Agent Client Protocol. Start here for a new adapter.
claude-sdkClaude Code, via the Agent SDK.
codex-app-serverCodex, via its app-server protocol.
pi-rpcpi's native RPC.
Most third-party CLIs implement acp, and it needs no atrium-side work beyond the manifest entry.

capabilities

Every capability defaults to false. The chat UI is capability-gated: a chip, card, or control never renders for an engine that cannot honor it, so under-declaring degrades gracefully while over-declaring produces controls that silently fail.

CapabilityUnlocks
resumeRestoring the session after a restart.
setModelThe model chip.
thinkingRendering reasoning output.
permissionCallbackInline approval cards.
commandDiscoveryThe / slash-command palette.
planModeThe plan toggle and plan cards.
contextUsageThe usage ring and the Compact action.
imageInputPaste / drag / attach images.
checkpointingHover Rewind with a dry-run preview.
subagentEventsSubagent drill-in rows.
steeringMid-turn message delivery.
backgroundTasksBackground-shell drill-in.

launchEnv and extraArgs

launchEnv injects environment variables into chat-transport spawns, and extraArgs appends arguments to the transport's argv. Their main job is suppressing the harness's own self-updater: a CLI that swaps its binary out from under a live session corrupts the conversation. A user's launch profile environment still wins on conflict.

updateCommand and methods.check_update

Declare methods.check_update and atrium periodically probes it for a newer harness version, surfacing an update chip in the launcher. Clicking it runs updateCommand in a visible terminal pane. See Adapters → Harness updates.

binaryDiscovery

Atrium resolves the binary at launch by trying, in order:

  1. Each entry in commands as a command on the user's $PATH.
  2. Each entry in wellKnownPaths (tilde-expanded, checked for existence and executability).
The first hit wins. If nothing is found, launching fails with a clear error pointing to where the binary is expected.

Include well-known paths for common Node version managers (nvm, fnm, volta) if your tool is installed via npm, and the Homebrew ARM path (/opt/homebrew/bin) for Apple Silicon.

sessions

Describes where the tool stores its session history so atrium can populate the resume picker.

  • pattern — glob relative to the user's home directory.
  • titleField — JSON field inside each session file to use as the human label.
  • idField — JSON field to use as the session ID (passed to resumeFlag).
If your tool doesn't persist sessions or the history is somewhere unusual, omit sessions. The launcher will simply not show a recent-sessions picker.

launch

Composes the command atrium runs in the pane.

  • base — argv prefix, for example ["my-tool"]. atrium substitutes the resolved binary path as argv[0].
  • resumeFlag — template applied when resuming. {session_id} is substituted with the stored session ID.
  • flagMap — maps launcher option IDs to argv fragments. {value} substitutes a select/text option's value.

launcherOptions

Inline UI controls shown in the launcher when this adapter is selected. Supported kinds:

  • toggle — boolean switch. Present in argv only when true (via flagMap).
  • select — single-select dropdown with options[] of {label, value}.
  • text — free-form text input.
Each option has an id (referenced by flagMap), label, and optional default. A select option's choices can be bare strings or { value, label } objects — the adapter owns the display label, so the launch-profile editor shows "Claude Fable 5" instead of the raw fable value, and an effort select renders friendly reasoning-effort levels.

suggestedFlags

Alongside options, an adapter can declare a suggestedFlags list that populates the launch-profile editor's "+ Add flag" picker — so a user editing a profile gets that tool's real CLI flags with descriptions rather than typing blind. Each entry is { flag, description?, values? }:

"suggestedFlags": [
  { "flag": "--add-dir", "description": "Additional directories to allow tool access" },
  { "flag": "--fallback-model", "description": "Fallback model on overload" },
  { "flag": "--output-format", "description": "Output format", "values": ["text", "json", "stream-json"] }
]

values advertises a closed set when the CLI has one; omit it for free-form flags.

Prior to SDK v2, launcher options were a separate launcher_options.json file. That form is still supported (reference it via methods.launcher_options = { "static": "launcher_options.json" }) and is where suggestedFlags and labeled choices live for the shipped adapters, but inline is preferred for options themselves.

icon

Adapters can ship a canonical brand SVG via a sibling file in the adapter directory; the manifest's icon field names that file ("icon.svg" is conventional). atrium loads the contents at adapter init and surfaces them through AdapterInfo.iconSvg, so the launcher tiles, Settings → Tools rows, the autocomplete facepile, and the activity sidebar all render the same brand mark.

Sanitizer rules (enforced at load time — failing any of these refuses the adapter):

  • Must have an <svg> root element. No HTML, no raw text, no other root nodes.
  • No <script> elements anywhere in the tree.
  • No <foreignObject> elements (they can host arbitrary HTML and CSS, including scripts).
  • No on*= event-handler attributes (onclick, onload, etc.).
  • No javascript: URIs in any attribute.
  • Hard size cap: 64 KB per file.
The root <svg> also has its width / height stripped and replaced with 100% / 100% so the host can size the icon via CSS. Shapes that use fill="currentColor" recolor against the surrounding text color (typically the adapter's accent); ship hard-coded brand colors only for surfaces that should stay branded regardless of theme.

Uninstalled adapters in the registry render the same SVG — the remote registry payload now embeds each adapter's icon body verbatim — so the icon stays consistent across the install transition. See Adapters → Overview for the installation flow.

Adding icon to an existing adapter is a manifest schema change: bump the adapter's version (minor) and set minAppVersion: 0.152.0. atrium's adapter schema has additionalProperties: false, so older clients that don't know about icon will refuse to load the adapter — the minAppVersion gate gives them a clean error instead.

hooks and hooks.sh

The hooks object lists which events your adapter emits. For each event, atrium registers an atrium://hooks/<adapter>/<event> endpoint.

hooks.sh is invoked once by atrium at install and uninstall time to let the adapter wire its hooks into the tool's own hook system. Contract:

#!/usr/bin/env bash
set -e

case "$1" in
  install)
    # Register this adapter's hooks with the tool.
    # Output JSON to stdout: { "subcommand": "install", "installed": true }
    ;;
  uninstall)
    # Remove the registrations.
    ;;
  status)
    # Report current state.
    ;;
esac

Timeout: 5 seconds for install/uninstall; hooks that exceed the timeout fail the operation but do not crash atrium.

Testing locally

  1. Clone atrium-adapters next to your atrium source tree.
  2. Drop your adapter directory into atrium-adapters/adapters/.
  3. Launch atrium's dev build (pnpm tauri dev). atrium checks the sibling path first in debug builds and will pick up your adapter immediately.
  4. Install it from Settings → Tools.
For a remote-installable adapter, open a PR to the atrium-adapters repository adding your adapter directory and an entry in registry.json.

Migration from SDK v1

If you have an SDK v1 adapter (four shell scripts + a minimal adapter.json), move each script's logic into the equivalent declarative block:

SDK v1 scriptSDK v2 equivalent
detect_running.shHandled automatically via process-tree introspection.
extract_session_id.shsessions.idField + adapter emits the session ID as part of session-start.
list_recent_sessions.shsessions.pattern + titleField + idField.
build_resume_command.shlaunch.resumeFlag.
hooks.sh is still needed for install/uninstall wiring. Everything else becomes JSON.