SNA

Feature × runtime matrix

Which SNA features work with Claude Code, Codex, OpenCode, Grok Build, and Cursor — with the caveats spelled out per row.

Every SNA feature flows through the same AgentProvider interface, but each runtime has its own native surface — so capabilities don't line up perfectly across the five. This page enumerates what actually works where, with the caveats spelled out per row.

If a row says , the field is accepted at the SNA API but the runtime either ignores it, falls back to respawn, or has no equivalent native surface.

Code audit: 2026-05-21, based on packages/core/src/core/providers/*, core/providers/acp/base.ts, and the runtime adapter test suite. Runtime notes still refer to Claude Code 1.x, Codex 0.x (app-server), OpenCode 0.x (serve), Grok Build CLI 0.1.x, and Cursor CLI 2026.05.

Runtime characteristics

CapabilityClaude CodeCodexOpenCodeGrok BuildCursor
Wire protocolclaude -p stream-json / --resume JSONLcodex app-server JSON-RPC stdioopencode serve HTTP + SDKgrok agent stdio ACP JSON-RPC stdiocursor-agent acp ACP JSON-RPC stdio
supportsRuntimePooling
supportsCwdPerThreadn/an/an/a
Public-standard protocol✗ vendor✗ vendor✗ vendorACPACP
Cold-start cost≈1s spawn per call≈2s daemon spawn, reused≈3s daemon spawn, reused≈2s spawn per session≈2s spawn per session

Claude Code, Grok Build, and Cursor are stateless per call/session — a fresh child process per spawn. Codex and OpenCode share daemons through RuntimePool. Only Codex's daemon is cwd-agnostic, which is why it's the only runtime that can host sessions for different working directories on a single pooled daemon. The two ACP-speaking runtimes (Grok Build and Cursor) share core/providers/acp/base.ts — see Runtimes for the split.

Session lifecycle

CapabilityClaude CodeCodexOpenCodeGrok BuildCursor
spawn(opts) → AgentProcess
Streaming AgentEvent during session
Active same-session continuation✓ same stream-json process✓ same Codex thread✓ same OpenCode session✓ same ACP session✓ same ACP session
interrupt() mid-turn✓ (ACP session/cancel)✓ (ACP session/cancel)
kill() / closeThread()✓ (pool refcount aware)✓ (pool refcount aware)
complete(opts) one-shot✓ (grok -p)✓ (cursor-agent -p)
complete() onDelta streaming✓ stream-jsonitem/agentMessage/deltaprompt_async SSE text deltas✓ streaming-json text events✓ stream-json assistant chunks (no model_call_id)
listModels()✓ static catalog✓ probes codex debug models + static fallback✓ probes opencode CLI✓ static (single entry: grok-build)✓ parses cursor-agent models (plain text) + static fallback

The "same-session continuation" row is the default chat path: call agent.send again on the same SNA session. resumeSessionId and canonical history replay are recovery/restart tools, not something a client should run on every turn.

OpenCode uses two paths for complete(): the sync SDK call when no callback is present, and prompt_async plus the event stream when onDelta is present. The streaming path resolves from the final text part and the assistant message metadata, so callers get both live text chunks and the final usage payload.

Event richness

Every runtime emits the same AgentEvent union, but the native stream does not expose the same granularity. This is the part that matters most for rich UIs: live tool-call cards, streaming JSON arguments, command output panes, token/cost meters, and durable replay after reload.

Event surfaceClaude CodeCodexOpenCodeGrok BuildCursor
Assistant text deltaassistant_delta from stream-jsonitem/agentMessage/deltamessage.part.delta✓ ACP agent_message_chunk✓ ACP agent_message_chunk
Final assistant eventassistant / resultitem/completed agent message✓ finalized text part✓ synthesized from ACP chunks before complete✓ synthesized from ACP chunks before complete
Thinking deltathinking_deltaitem/reasoning/summaryTextDelta✓ reasoning message.part.delta✓ ACP agent_thought_chunk✓ ACP agent_thought_chunk
Final thinking event✓ signed thinking block✓ completed reasoning item✓ finalized reasoning part✓ synthesized from ACP thought chunks✓ synthesized from ACP thought chunks
Content lifecycle starts✓ ignored unless the block is a toolagentMessage / reasoning starts are ignored✓ text/reasoning part starts are tracked, not emitted as tools✓ message/thought chunks are content, not tools✓ message/thought chunks are content, not tools
Tool call starttool_use with streaming: true✓ typed item start (shell, file_change, MCP, hosted tools)✓ tool part running/pending✓ ACP tool_call✓ ACP tool_call
Tool input deltatool_use_delta from Anthropic input_json_delta✗ no native input delta✗ no native input delta⚠ ACP tool_call_update can refresh input, but not byte/token deltas⚠ ACP tool_call_update can refresh input, but not byte/token deltas
Tool output streaming✗ final tool_result only✓ shell outputDelta as tool_use update; file patch delta is dropped✗ final part status only✗ final/update payload only✗ final/update payload only
Tool result payloadtoolUseId, isError, text/JSON content✓ rich per item: exitCode, status, durationMs, MCP error, web_search, image_generation.savedPath, revisedPrompt, unknown-tool rawtitle, isError, output/error✓ ACP rawOutput, locations, status, kind, plus Grok tool-name unwrap✓ ACP rawOutput, locations, status, kind, plus Cursor tool-kind unwrap
Session completion metadata✓ duration, API duration, cost, token usage, cache usage, model, context window✓ duration and token/cache usage; cost is not reported⚠ session stream only carries the provider field; one-shot complete() returns usage/cost when OpenCode reports it✓ ACP stopReason only✓ ACP stopReason only
Unknown/new tool visibility✓ native tool name still surfaces✓ fail-open generic tool_use / tool_result with raw payload✓ tool-like unknown SSE events fail open as tool_use, tool_use_delta, or tool_result with raw properties✓ tool-like unknown ACP updates fail open with rawUpdate✓ tool-like unknown ACP updates fail open with rawUpdate

The most important asymmetry is tool_use_delta: only Claude Code currently exposes streaming partial JSON for tool inputs. Codex exposes richer item lifecycle metadata and live shell output, but not partial tool arguments. The ACP runtimes expose useful tool_call_update refreshes and final raw outputs, but SNA treats them as whole-event updates rather than fine-grained deltas.

Assistant text and reasoning lifecycle starts are never surfaced as tool_use. A runtime may notify SNA when a content block starts, but only real tool invocations enter the tool_use / tool_result contract.

In-place mutation (applyPatch)

Each runtime declares which SessionPatch fields can be applied in place without a respawn. Fields returned as "leftover" require the session manager to kill and re-spawn with history replay.

FieldClaude CodeCodexOpenCodeGrok BuildCursor
model✓ control_request set_model✓ next-turn override✓ next-prompt override✗ leftover (respawn)✗ leftover (respawn)
permissionMode✓ control_request✓ next-turn sandbox policy✓ next-prompt agent override✓ SNA permission gate update✓ SNA permission gate update
cwd✗ leftover✓ next-turn override (the only runtime that can)✗ leftover✗ leftover✗ leftover

ACP-speaking runtimes still return model and cwd as leftover because ACP does not expose those in-session mutators in the version used by Grok Build / Cursor. permissionMode is different: SNA owns the ACP permission gate, so switching to or from bypassPermissions can be applied in-place without restarting the native process.

Permission flow

CapabilityClaude CodeCodexOpenCodeGrok BuildCursor
permission_needed AgentEvent emit✓ via hook bridge✓ via JSON-RPC server-request✓ via SSE permission.updated✓ via ACP session/request_permission✓ via ACP session/request_permission
respondToPermission(id, approved)✗ external hook script handles approval✓ JSON-RPC response✓ POST /permissions/:id✓ ACP {outcome:{outcome:"selected",optionId}}✓ ACP {outcome:{outcome:"selected",optionId}}
Bidirectional (runtime waits on us)✗ hook script makes the decision
bypassPermissions auto-approve✓ hook script always allows✓ auto-responds to server requests✓ auto-replies through the HTTP permission API✓ shared ACP base auto-replies allow_*✓ shared ACP base auto-replies allow_*

Claude Code's permission model is out-of-band — SNA writes a hook script the CLI calls, the script decides on its own (based on permissionMode and allow/deny rules), and the CLI never waits on the SNA process. The other four runtimes are bidirectional: the agent's turn suspends until SNA sends a permission response. respondToPermission is a no-op for Claude Code by design.

Configuration knobs (SpawnOptions)

FieldClaude CodeCodexOpenCodeGrok BuildCursor
systemPrompt--system-promptbaseInstructions on thread/start✓ joined with appendSystemPrompt✓ ACP base first-turn resource block (the headless complete() path also supports --system-prompt-override)✓ ACP base first-turn resource block (no CLI flag exists; the fold rides on the shared base)
appendSystemPrompt--append-system-promptdeveloperInstructions on thread/start✓ joined with systemPrompt✓ joined with systemPrompt in the same first-turn block (complete-path also supports --rules)✓ joined with systemPrompt in the same first-turn block
permissionMode: "plan"✓ native --permission-mode plan✗ no plan surface; maps to read-only sandbox✓ maps to OpenCode agent plan✗ no adapter mapping today✗ no adapter mapping today
allowedTools✓ native --allowedTools✓ via PreToolUse hook (deny-others)⚠ not a strict allowlist; listed tools are enabled but others stay at OpenCode defaults✓ ACP base auto-denies non-listed tools at the session/request_permission boundary✓ ACP base auto-denies non-listed tools at the session/request_permission boundary
disallowedTools✓ native --disallowedTools✓ via PreToolUse hook✓ disables listed tools✓ ACP base auto-denies listed tools at the permission boundary✓ ACP base auto-denies listed tools at the permission boundary
mcpServers--mcp-config JSON✓ written to config.toml [mcp_servers.*]✓ injected into daemon config via SDK✓ stdio→HTTP bridge + <cwd>/.grok/config.toml✓ stdio→HTTP bridge + <cwd>/.cursor/mcp.json (merges with existing user entries, restores on exit)
reasoningLevel (0..5)toClaudeEffort--efforttoCodexEffortmodel_reasoning_effort✗ ignoredtoGrokEffort--effortapplyCursorReasoning → model-id suffix (gpt-5.3-codexgpt-5.3-codex-high)
configDirCLAUDE_CONFIG_DIRCODEX_HOME✗ no equivalent✗ no equivalent (Grok Build uses ~/.grok)✗ no equivalent (Cursor uses ~/.cursor; CURSOR_CONFIG_DIR only reroutes cli-config.json, not hooks)
resumeSessionId✓ native --resume <sessionId>thread/resume API✓ existing OpenCode session id✗ not wired in the adapter✗ not wired in the adapter

providerOptions (runtime-specific escape hatches)

SNA promotes an option into a runtime-neutral field only when every runtime can honor the same semantics. API endpoint overrides do not meet that bar: Codex can be pointed at OpenAI-compatible gateways through its native -c model_providers.* configuration, Claude Code uses Anthropic-specific environment variables, OpenCode has its own config file, and Cursor currently has no SNA-supported base URL override. Use the runtime-native escape hatch instead of a fake shared apiBaseUrl.

KeyClaude CodeCodexOpenCodeGrok BuildCursor
settings✓ merged into --settings JSON✓ only hooks.PreToolUse is honored
settingSources--setting-sources
strictMcpConfig--strict-mcp-config
maxTurns--max-turns
disableSlashCommands--disable-slash-commands
serviceTier✗ would mis-bill (/fast is a model, not a tier)priority / flex / batch
profile✓ passed as --profile to codex app-server and codex exec; also isolates the runtime pool
config (-c key=value)✓ passed to codex app-server and codex exec; use this for OpenAI-compatible gateways such as OpenRouter or local model servers
hooksHash / configHash✓ manual runtime pool isolation keys for generated hooks/config
serverUrl✓ skip spawn, attach to existing daemon
modelProviderId✓ provider half of {providerID, modelID}
agent✓ build / plan / etc.
opencodeSessionId✓ resume existing session before creating one
logLevel✓ daemon config logLevel via SDK
opencodeConfigHash✓ runtime pool isolation key

Cursor has no SNA-specific providerOptions keys today — every knob the CLI accepts (model, reasoning, mode) maps to a typed SpawnOptions field. New escape hatches will surface here if a future Cursor flag needs them.

Cross-runtime history replay

Every runtime has its own mechanism for seeding a fresh session with a prior conversation. SNA's canonical CanonicalBlock[] history is translated into the native shape at the adapter layer.

RuntimeMechanismAdapter
Claude CodeWrite Anthropic-format JSONL, pass path to --resume <filepath>history/claude-code.ts
Codexthread/resume(history=ResponseItems) JSON-RPChistory/codex.ts
OpenCodePrepend a history prelude TextPartInput (plus file parts for embeds) to the first prompthistory/opencode.ts
Grok Build / CursorSerialise transcript into a single ACP resource content block on the first session/promptshared in core/providers/acp/base.ts (serializeHistoryForAcp + buildHistoryPromptBlock)

The ACP path is intentionally different from the native-resume paths: probing showed that ACP's session/load only replays UI events without restoring model context, and the underlying session storage is vendor-internal (xAI for Grok, SQLite under ~/.cursor/chats/ for Cursor) and gets regenerated on load — so we avoid touching the storage layer entirely. Instead we embed the prior transcript inline on turn 1; the context persists across subsequent turns within the same session, so re-injection is not required.

Known gaps and validation limits

  • Plan mode is not uniform: Claude Code supports native plan mode, and OpenCode maps it to the plan agent. Codex treats plan like the default read-only sandbox, and the Grok Build / Cursor adapters do not wire a plan-mode flag today. ACP runtimes can change SNA-side permission gate behavior in-place, but that is not the same as a native plan mode.
  • Glob/pattern matching in allowedTools / disallowedTools for the ACP runtimes — today the auto-deny intercept matches tool names by exact string equality. Glob patterns (e.g. "shell.*") are a natural follow-up if real workloads need them.
  • Image input minimum dimensions — Grok Build rejects images smaller than 8×8 pixels or below 512 total pixels (square images need at least 23×23); Cursor accepts as small as 16×16. Both validate server-side, so SNA passes the image block through unchanged. Consumers feeding tiny thumbnails should upscale to at least 32×32 to satisfy Grok.

On this page