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
| Capability | Claude Code | Codex | OpenCode | Grok Build | Cursor |
|---|---|---|---|---|---|
| Wire protocol | claude -p stream-json / --resume JSONL | codex app-server JSON-RPC stdio | opencode serve HTTP + SDK | grok agent stdio ACP JSON-RPC stdio | cursor-agent acp ACP JSON-RPC stdio |
supportsRuntimePooling | ✗ | ✓ | ✓ | ✗ | ✗ |
supportsCwdPerThread | n/a | ✓ | ✗ | n/a | n/a |
| Public-standard protocol | ✗ vendor | ✗ vendor | ✗ vendor | ✓ ACP | ✓ ACP |
| 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
| Capability | Claude Code | Codex | OpenCode | Grok Build | Cursor |
|---|---|---|---|---|---|
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-json | ✓ item/agentMessage/delta | ✓ prompt_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 surface | Claude Code | Codex | OpenCode | Grok Build | Cursor |
|---|---|---|---|---|---|
| Assistant text delta | ✓ assistant_delta from stream-json | ✓ item/agentMessage/delta | ✓ message.part.delta | ✓ ACP agent_message_chunk | ✓ ACP agent_message_chunk |
| Final assistant event | ✓ assistant / result | ✓ item/completed agent message | ✓ finalized text part | ✓ synthesized from ACP chunks before complete | ✓ synthesized from ACP chunks before complete |
| Thinking delta | ✓ thinking_delta | ✓ item/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 tool | ✓ agentMessage / 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 start | ✓ tool_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 delta | ✓ tool_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 payload | ✓ toolUseId, isError, text/JSON content | ✓ rich per item: exitCode, status, durationMs, MCP error, web_search, image_generation.savedPath, revisedPrompt, unknown-tool raw | ✓ title, 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.
| Field | Claude Code | Codex | OpenCode | Grok Build | Cursor |
|---|---|---|---|---|---|
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
| Capability | Claude Code | Codex | OpenCode | Grok Build | Cursor |
|---|---|---|---|---|---|
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)
| Field | Claude Code | Codex | OpenCode | Grok Build | Cursor |
|---|---|---|---|---|---|
systemPrompt | ✓ --system-prompt | ✓ baseInstructions 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-prompt | ✓ developerInstructions 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 → --effort | ✓ toCodexEffort → model_reasoning_effort | ✗ ignored | ✓ toGrokEffort → --effort | ✓ applyCursorReasoning → model-id suffix (gpt-5.3-codex → gpt-5.3-codex-high) |
configDir | ✓ CLAUDE_CONFIG_DIR | ✓ CODEX_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.
| Key | Claude Code | Codex | OpenCode | Grok Build | Cursor |
|---|---|---|---|---|---|
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.
| Runtime | Mechanism | Adapter |
|---|---|---|
| Claude Code | Write Anthropic-format JSONL, pass path to --resume <filepath> | history/claude-code.ts |
| Codex | thread/resume(history=ResponseItems) JSON-RPC | history/codex.ts |
| OpenCode | Prepend a history prelude TextPartInput (plus file parts for embeds) to the first prompt | history/opencode.ts |
| Grok Build / Cursor | Serialise transcript into a single ACP resource content block on the first session/prompt | shared 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
planagent. Codex treatsplanlike 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/disallowedToolsfor 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.