Coding harnesses (code-session backends)
Neo's code-session tool drives an external (or internal) coding agent as a live, multi-turn session. Each backend is a CodeAgentTransport (src/tools/code-harness/transport.ts) carrying a static capability descriptor (capabilities.ts) plus a live availability probe. A driving persona calls op:'list-backends' to see this matrix at runtime (with a live probe + track record) and adapt its brief per each backend's briefingNotes.
This doc is the human-facing companion. Rows for claude-code/codex/neo are sourced from capabilities.ts; opencode from opencode-cli.ts (live-verified against opencode-ai 1.17.18); copilot from copilot-cli.ts (live-verified against @github/copilot 1.0.70); droid from droid-cli.ts (live-verified against Factory Droid v0.169.0); grok from grok-cli.ts (chip R-xai, the argv surface live-verified against grok 0.2.93 (f00f96316d); the SUCCESS-path JSON event schema was ALSO live-verified in a follow-up capture chip, 2026-07-12 — see its "Install & auth" entry below).
Chip H4-C ran a real supervised session through every backend on Windows (2026-07-10). claude-code / codex / opencode / copilot / neo all completed the add-a-function smoke and captured a git diff; droid was exercised unauthenticated and produced the expected clean auth-error. Store, live SSE feed, guidance round-trip, kill→restart→orphaned→resume, and the lesson distiller were all verified live. One real drift was fixed: opencode needs --dir (below). Auth reality on the test box AT THAT TIME: opencode had 0 provider credentials (drove with the free OpenCode Zen opencode/big-pickle model); droid needed FACTORY_API_KEY for its own hosted models. Refreshed 2026-07-12 (chip R-route): opencode is no longer auth-blocked — opencode auth list now reports an OpenRouter credential (~/.local/share/ opencode/auth.json, 1 credential, live-verified), so a provider/model selection like openrouter/<id> is drivable, not just the free Zen fallback. Separately, droid's own FACTORY_API_KEY requirement applies only to its HOSTED models — a BYOK custom:<model-id> (from ~/.factory/settings.jsoncustomModels[]) bypasses Factory auth entirely (scratch/droid-byok- research.md); see the Factory Droid section below.
Capability matrix
| Backend | id | Process model | Granularity | Per-tool gating (honorsAllowedTools) | Mid-turn clarify | Resume | Model selection | Cost | Live: interrupt / set-posture / guide-mid-turn |
|---|---|---|---|---|---|---|---|---|---|
| Claude Code | claude-code | persistent SDK query() | stream (per-model-turn) | ✅ yes (SDK allowedTools) | ✅ yes (canUseTool) | native | per-turn (query.setModel) | subscription | ✅ / ✅ / ❌ |
| OpenAI Codex | codex | per-turn codex exec subprocess | per-turn | ❌ no (sandbox is the boundary) | ❌ no | thread-id (resume <id>) | per-turn (fresh argv/turn) | subscription | ✅ / ✅ / ❌ |
| Neo internal | neo | in-process agent-loop round | per-turn | ✅ yes (intersected) | ✅ yes (ask-parent bridged) | none | per-session (same provider — Wave J F1) | internal | ✅ / ✅ / ❌ |
| OpenCode | opencode | per-turn opencode run subprocess | per-turn | ❌ no (posture is the boundary) | ❌ no | native (--session <ses_id>) | per-session | api-metered | ✅ / ✅ / ❌ |
| GitHub Copilot | copilot | per-turn copilot -p subprocess | per-turn | ✅ yes (--available-tools) | ❌ no (-p one-shot) | native (--session-id/--resume) | per-session | subscription | ✅ / ✅ / ❌ |
| Factory Droid | droid | per-turn droid exec -o json subprocess | per-turn | ❌ no (--auto tier is the boundary) | ❌ no | native (-s <session_id>) | per-session | api-metered | ✅ / ✅ / ❌ |
| xAI Grok Build | grok | per-turn grok -p --output-format json subprocess | per-turn | ❌ no (--tools live-verified to crash session creation, CR-R) | ❌ no (-p one-shot) | native (-s/--resume/--continue) | per-session | api-metered | ✅ / ✅ / ❌ |
honorsAllowedTools:false backends (codex, opencode, droid, grok) REJECT an explicit allowedTools on op:'start' loudly rather than silently ignoring it — their sandbox/posture is the only boundary. Grok's case (CR-R, 2026-07-12) is different from the other three: the CLI documents --tools as a real allow-list flag, but live re-verification against the installed binary found it crashes session creation for any value (see grok-cli.ts's module doc for the reproduction) — so it is treated the same as "no per-tool gating" even though the flag exists on paper.
Selection: capabilities + availability + track record
op:'list-backends' returns, per backend, the rendered descriptor above, a LIVE probeAvailability() result, AND — since Wave H3-D — a trackRecord line distilled from how past sessions on that backend actually went (buildHarnessTrackRecordLines, src/observability/harness-outcomes.ts; TTL-cached ~60s, omitted when the backend has no history). The driver persona weighs all three: it never picks an available:false backend, and prefers the one whose track record shows it does well on work like the task at hand. See docs/code-session.md §7 for the learning loop that produces those lines.
What the DRIVEN agent brings along (repo-local capabilities)
Some backends load repo-local capabilities on their own, on top of whatever the brief says: claude-code picks up the target repo's .claude/skills/, CLAUDE.md, and .mcp.json; codex reads the repo's AGENTS.md and its own config.toml; the in-house neo backend runs with Neo's OWN skill/tool set (intersected with the session's allowedTools ceiling). The driving persona does not manage these today — a brief should therefore assume the driven agent may know repo conventions the brief never stated. Per-project CURATION of these capabilities (provisioning exactly the skills/MCP servers a given repo needs, from official pinned sources) is the Wave-I initiative — see scratch/research-capability-sources.md once its research chip lands.
Diff capture per backend (editToolNames)
Per-turn diff capture (src/tools/code-harness/diff-capture.ts, chip H3-C) is hybrid: it prefers a git diff of the session cwd, and falls back to accumulating edited paths from tool_use events whose tool name is in the backend descriptor's editToolNames. So a backend with a rich editToolNames list still yields a file list even in a non-git dir, while a backend that emits no per-tool events leans entirely on the git path:
| Backend | editToolNames | Fallback when git is unavailable |
|---|---|---|
claude-code | Write, Edit, MultiEdit, NotebookEdit | file list from tool events |
codex | apply_patch | file list from tool events (file_change items normalize to apply_patch) |
neo | write-file, edit-file | file list from tool events |
opencode | write, edit, patch | file list from tool events |
copilot | apply_patch, write | file list from tool events (each edit's execution_complete also carries a unified diff) |
droid | (none) | git path only — droid exec -o json emits one final result object, no per-tool events |
grok | (none) | git path only — the json output mode this transport uses emits one terminal object, no per-tool events |
Posture mapping
Neo's four postures (plan < default < acceptEdits < full) map to each backend's own permission representation:
| Neo posture | claude-code (SDK mode) | codex (--sandbox) | neo (PermissionMode) | opencode (run flags) | copilot (-p flags) | droid (--auto) | grok (--permission-mode) |
|---|---|---|---|---|---|---|---|
plan | plan | read-only | plan | --agent plan (read-only agent) | --mode plan --allow-all-tools (read-only planner) | (no --auto — read-only) | plan |
default | default | workspace-write | default | (no flag — build agent, permissive) | --allow-all-tools --deny-tool=shell --deny-tool=write --deny-tool=apply_patch (read/search only) | --auto low | default |
acceptEdits | acceptEdits | workspace-write | acceptEdits | (no flag — same as default) | --allow-all-tools --deny-tool=shell (edits auto-approved, shell denied) | --auto medium | auto |
full | bypassPermissions | danger-full-access | bypass | --auto (auto-approve non-denied) | --allow-all | --auto high | bypassPermissions |
Grok's --permission-mode enum spells three of Neo's own posture names VERBATIM (plan/default/acceptEdits), but acceptEdits is a live-verified NAMING TRAP (2026-07-12, scratch/grok-live-capture.md): headless -p mode has no interactive approval channel, and grok's own acceptEdits value CANCELS any tool call that needs approval instead of auto-approving it — an edit task run under it left the target file unchanged. --permission-mode auto is the value that actually auto-approves and completes the turn unattended, so Neo's acceptEdits posture maps to grok's auto, not its own acceptEdits string. full maps to bypassPermissions, matching the Claude Code SDK naming grok's own --allow/--deny help text explicitly cross-references ("Claude Code: --allowedTools"). See the "Known quirks" section below for the full reproduction (both acceptEdits and dontAsk were confirmed to cancel).
OpenCode posture notes (important)
opencode run is non-interactive and exposes only two real inline posture levers: --agent plan (read-only) and --auto (auto-approve). There is NO --config/--allowed-tools flag and no answer channel, so an ask permission would HANG a driven turn — Neo never requests one. Consequences:
default/acceptEditsare NOT "ask first". They defer to opencode's own permission config, which for a stock install is permissive (*:allowon the built-inbuildagent) — edits proceed without a hang. A user's ownopencode.jsondenyrules still apply. Treatdefaulthere as "let it edit", not "prompt me".- A per-session temp
opencode.jsonwas considered (to synthesize an ask/acceptEdits gradient) and rejected:runhas no--configflag, and writingopencode.jsoninto the session cwd would pollute the user's repo and race concurrent sessions. Finer per-tool control lives inopencode serve's REST/SSE surface — a future option, deferred to chip H4-C.
Model + effort routing (Wave J)
op:'start' (and, for per-turn backends, op:'guide') accept two routing knobs the driving persona sets per task — agentModel and effort — so a cheap/fast tier is used when it suffices and a frontier model + high effort is reserved for genuinely hard work. The knowledge lives as DATA (per-backend descriptor fields + routing-catalog.ts), and the decision stays with the persona (see templates/agents/code-specialist.md's "Choosing model + effort").
agentModel— a concrete backend model id, OR one of the model-CLASS sentinelscheap/standard/frontier(ModelTier,providers/model-router.ts). A sentinel is resolved atstarttime to the backend's concrete id via its descriptormodelClassMap(capabilities.ts); a sentinel with no mapping (anull/absent entry — the copilot auto-model precedent) or backendneo(which inherits Neo's own provider) collapses to "omit the flag, let the backend auto-pick". The post-resolution CONCRETE id is what is persisted onto the session record and the outcome (never the sentinel).effort—low/medium/high/max(CC-parity naming). Accepted only by a backend whose descriptoreffortSupportnames a live-verified mechanism; an expliciteffortagainst a backend with'none'/'unknown'(or the field absent) is REJECTED loudly at the tool layer, the same posture as theallowedTools/posture-ceiling gates — never silently dropped.
Per-backend effort mechanism (design §3; J7 live-verified 2026-07-10, J8/J8b wired 2026-07-10)
Canonical Neo level → native knob. Chip J7 live-verified every row against the installed CLIs/SDK on Windows (captures in scratch/j7-live-capture.md); chip J8 wired the levers J7 discovered on opencode/copilot (droid shipped a dormant seam — its --help names the flag but not the accepted values); J8b closed the droid gap with a free argv-parse probe (droid exec -r bogus-value "noop" fails at parse with the full enum, before any auth/billing path — capture item 6b) and flipped the seam live. All six backends now accept effort.
| Backend | Mechanism | low / medium / high / max | Status |
|---|---|---|---|
neo | internal thinking plumbing → CompletionRequest.thinking:{type:'enabled',effort} (claude-provider.ts) | low / medium / high / max→high (clamped) | KNOWN — pure internal plumbing, live |
claude-code | SDK query() Options.effort (verified effort?: EffortLevel, sdk.d.ts:1593) — start-time only; query.setModel(model?) takes no effort arg (sdk.d.ts:2242) | low / medium / high / max | WIRED (J7) — start-time effort live; per-turn effort loud-rejected (set on start) |
codex | -c model_reasoning_effort="<level>" per turn (verified via codex exec --help + codex-src enum) | low / medium / high / max→xhigh | WIRED (J7) — start + per-turn effort live |
opencode | --variant <value> per turn (opencode run --help: provider-specific reasoning effort, e.g. high/max/minimal) — Neo's own level names passed through verbatim (no closed enum to translate against) | low / medium / high / max (pass-through, unmapped) | WIRED (J8) — re-applies to every turn (per-turn respawn); no live setModel — modelSelection stays 'per-session', mid-session switch still rejected |
copilot | --effort <level> per turn (copilot --help closed enum: none/minimal/low/medium/high/xhigh/max) | low / medium / high / max (1:1) | WIRED (J8) — re-applies to every turn; --model/auto UNCHANGED (modelClassMap stays all-null — J7's H4-C caveat stands) |
droid | -r/--reasoning-effort <level> per turn (enum captured via parse probe: none/dynamic/off/minimal/low/medium/high/xhigh/max; honored in -o json mode) | low / medium / high / max (1:1 — max→max, droid's own top literal, unlike codex) | WIRED (J8b) — re-applies to every turn; --use-spec is an orthogonal workflow knob |
grok | --reasoning-effort <level> (alias --effort) per turn (enum captured via a free argv-parse probe: none/minimal/low/medium/high/xhigh/max, or a model menu option id) | low / medium / high / max (1:1 — max→max, grok's own top literal, same pattern as droid) | WIRED (chip R-xai) — re-applies to every turn (per-turn respawn) |
Per-turn switching (op:'guide' with agentModel/effort) is v1-supported on claude-code (MODEL only — per-turn effort is loud-rejected, see above) and codex (model AND effort) via modelSelection:'per-turn'; every other backend (including opencode/copilot/droid, now that their start-time effort is wired) rejects a mid-session switch loudly — set the model/effort on start. The three per-turn-respawn backends need no live switch to keep effort in force for the whole session: each re-spawns a fresh process every turn and reads the SAME start-time effort into each turn's argv, so the level naturally persists without a setModel primitive.
op:'list-backends' renders all of this per backend as routingGuidance lines (sibling of trackRecord) so the persona sees, at selection time, each backend's effort support, cost-profile framing (subscription = speed/rate-limit play; metered = dollar play), and how each class sentinel resolves.
Install & auth
Claude Code (claude-code)
- No install: the Claude Agent SDK bundles its own binary. Availability = "does the SDK module load".
- Auth: rides the Claude Code login (no
ANTHROPIC_API_KEYneeded).
OpenAI Codex (codex)
- Install:
npm i -g @openai/codex. - Auth:
codex login. Neo reuses~/.codex/auth.jsonread-only.
Neo internal (neo)
- No install/auth — runs in-process on Neo's own configured provider/token.
OpenCode (opencode)
- Install:
npm install -g opencode-ai(also choco/scoop). Docs recommend WSL on Windows, but native npm works — Neo spawns the.cmd/.ps1shim through thecli-harness.tsexeca toolkit, which handles the Windows shim quirks (opencode #8160/#17295). - Auth per-provider:
opencode auth login(or provider env keys, or the OpenCode Zen hosted marketplace). Claude Pro/Max OAuth is a community plugin only — don't rely on it. - Availability probe caveat:
opencode --versionsucceeds even with NO provider credential configured, solist-backendsreportsavailable:true; an auth/credential failure surfaces at run time as anerrorevent (error.data.message) + non-zero exit, normalized to aCodeAgentEventerror. Same posture as codex.
GitHub Copilot (copilot)
- Install:
npm install -g @github/copilot(Node 22+). Docs say Windows wants PowerShell 6+, but Neo spawns thecopilotshim through thecli-harness.tsexeca toolkit and it works from a stock shell — no pwsh install needed for our spawn path. - Auth:
/logindevice flow, or a token inCOPILOT_GITHUB_TOKEN>GH_TOKENGITHUB_TOKEN(a fine-grained PAT needs the "Copilot Requests" permission).copilot --versiondoes NOT check auth, solist-backendsshowsavailable:trueeven un-authed; an auth failure surfaces at run time as anerrorevent + non-zero exit. Same posture as codex/opencode.
Factory Droid (droid)
- Install:
npm i -g droid(the npmdroidpackage IS Factory's CLI — verified v0.169.0) or the PowerShell installerirm https://app.factory.ai/cli/windows | iex. - Auth:
FACTORY_API_KEY=fk-...(app.factory.ai/settings/api-keys) or the interactive/logindevice flow. Neo does NOT persist the key; it inherits the ambient env.droid --versionsucceeds regardless of auth, solist-backendsshowsavailable:true; an unauthenticated run surfaces as anerrorevent carrying droid's "Authentication failed…" message. - BYOK bypasses FACTORY_API_KEY entirely (live-verified 2026-07-12,
scratch/droid-byok-research.md): droid supports "Bring Your Own Key" custom models viacustomModelsin~/.factory/settings.json(model,baseUrl,apiKey— supports${VAR_NAME}env-ref syntax,provider: "generic-chat-completion-api"for any OpenAI-compatible endpoint — https://docs.factory.ai/cli/byok/overview). Selecting the model ascustom:<model-id>(via-m/--model, already howbuildDroidArgsforwardso.modelindroid-cli.ts) routes the ENTIRE turn to the BYOK provider — live-verified end-to-end against Novita (tencent/hy3) with bothFACTORY_API_KEYunset and~/.factory/auth.v2.file/auth.v2.keytemporarily removed (zero Factory account state of any kind):droid exec --model custom:tencent/hy3 -o json --auto low "..."returned a cleantype:"result"success. So FACTORY_API_KEY (and even a free Factory account login) is required only for droid's own hosted models (claude-opus-4-8etc.) — a BYOK custom model needs neither. No code change was needed indroid-cli.ts:probeAvailabilitynever gated onFACTORY_API_KEY(onlydroid --version, which already succeeds unauthenticated), andbuildDroidArgsalready forwards an arbitraryo.modelstring opaquely to-m, socustom:<id>was already wireable. - Mode: per-invocation
droid exec -o json(one finaltype:"result"object per turn).stream-jsonrpcis the future liveControl upgrade (mid-run steering, interrupts, live permission answering) — see the droid-cli.ts module doc.
xAI Grok Build (grok)
- Install:
irm https://x.ai/cli/install.ps1 | iex(Windows) orcurl -fsSL https://x.ai/cli/install.sh | bash(macOS/Linux/WSL) — the OFFICIAL vendor installer (docs.x.ai/build/overview, x.ai/cli). Installs to~/.grok/bin/grok.exe(+agent.exe) and adds it to the user PATH. Live-verified: grok 0.2.93 (f00f96316d), Windows x86_64, 2026-07-12. - Auth:
grok login(OAuth via auth.x.ai, browser or--device-authfor headless/remote), or anXAI_API_KEYenv var (console.x.ai, checked "when local login unavailable" per docs.x.ai/build/cli/reference). Rides either a SuperGrok/X Premium Plus subscription (OAuth path) or metered API billing (key path) — seeGROK_CAPS.costProfile's note ingrok-cli.tsfor why this transport assumes the metered path. xAI is running a time-limited promotion making Grok 4.5 the free default model in the CLI (x.ai/news/ grok-4-5, x.ai/news/grok-build-cli) — a promotion, not a permanent free tier; it still requires either OAuth login or an API key, neither of which Neo can obtain unattended (no account creation, no completing an OAuth flow). grok --versiondoes NOT require auth (prints the version regardless), solist-backendsreportsavailable:trueeven fully unauthenticated — same posture as codex/opencode/copilot/droid. An auth failure surfaces at run time as atype:"error"event (the one JSON shape this chip live-captured — see below) or a non-zero exit, normalized via the shared exit-error synthesizer.- The SUCCESS-path JSON event schema is now LIVE-VERIFIED (
scratch/grok-live-capture.md, 2026-07-12, owner-authenticated~/.grok/auth.json, two tiny headless turns — full byte-for-byte transcript in the worksheet).grok -p "Reply with exactly the single word PONG97531…" --output-format json -s <uuid>againstgrok-4.5returned exactly{"text":"PONG97531","stopReason":"EndTurn","sessionId":"<uuid>", "requestId":"<uuid>","thought":"…"}— notypefield at all (unlike the error envelope, which always hastype:"error"). A second turn via--resume <uuid>confirmed the same shape,sessionIdunchanged. The earlier-captured error shape: supplying a syntactically-valid but WRONGXAI_API_KEYtogrok -p "hi" --output-format jsonreturns exactly{"type":"error","message":"Internal error: {...API error (status 400 Bad Request): invalid-argument: Incorrect API key provided...}"}— normalized 1:1 innormalizeGrokEvent. Since the success envelope never carries atypefield,ev.type === 'error'is a clean, unambiguous discriminator between the two live-verified shapes.grok-cli.tsextracts response text from the (now-verified-first)textfield, withresult/response/content/message/thoughtkept as forward-compatible fallbacks for a future CLI release. The turn boundary still does not gate on recognizing a field: likeopencode(chosen there for terminal-event flakiness, #26855; chosen here because--output-format jsonprints exactly one terminal object with no earlier "more to come" marker),turn_completeis synthesized from the clean process exit — this keeps the transport robust to a future schema change without another live-capture chip. - CRITICAL:
--output-format jsonis PRETTY-PRINTED, not JSONL — the transport buffers, it does not parse per-line. Live-verified 2026-07-12 (raw execa capture,scratch/grok-live-capture.md): grok writes its terminal object with embedded newlines and indentation ({\n "text": "…",\n …\n}), unlike every other Wave-H2 CLI backend here (droid/copilot emit compact single-line JSON per event). Confirmed by driving a REAL supervisedcode-sessionthrough thegrokbackend end-to-end: the shared readline-based line pump splits that output into several fragment lines, and none of them individually parse as JSON — a naive per-line parser (the pattern every other backend uses) would silently extract ZERO text on every single turn, even a fully successful one (confirmed happening in practice before the fix: the driven session'sturn_completeevents all carriedtext:""despite the file actually being edited correctly, per the git diff).grok-cli.tsnow buffers each turn's lines and parses the REJOINED whole-turn text exactly once, after the process closes stdout — safe because--output-format jsonalways prints exactly one terminal object per invocation, never a real multi-line event stream. - Session model: PINNED, not parsed (copilot's pattern, not droid's) — the transport generates a UUID and passes it via
-s <uuid>on a fresh turn 1 (grok's own--help: "must be a valid UUID and must not already exist... Does not resume existing sessions — use--resume/--continueinstead"), then--resume <uuid>on every later turn. Live-verified: the success envelope's ownsessionIdfield echoed the pinned uuid back exactly on both turns, confirming the "don't need to parse it" design was correct — the field is read-only diagnostic confirmation, never consumed. - Model:
grok models(live, 2026-07-12) lists exactly one model on the capturing account —grok-4.5(~/.grok/models_cache.json:"description":"SpaceXAI's new frontier model",context_window:500000,reasoning_efforts: high(default)/medium/low) — the free-promo model this chip evaluated.GROK_CAPS.knownModels = ['grok-4.5'],modelClassMap = { frontier: 'grok-4.5' }(no cheaper model observed, socheap/standardresolve to "omit the flag"). - Effort probe caveat (own finding): running the free
--reasoning-effortargv-parse probe (grok -p noop --reasoning-effort bogus-value) transiently triggered grok's OAuth device-code prompt before exiting on its own with the argv error — grok resolves "or a model menu option id" by trying to fetch the live model menu, which needs auth. No browser was opened and no code was entered; the enum text still surfaced in the final error message. Recorded here so a future re-probe expects this and doesn't mistake it for a hang.
Known quirks
- OpenCode terminal-event flakiness (#26855 / #29997):
opencode runcan exit before emitting its finalstep_finish{reason:'stop'}and may never emit the user prompt message. Neo therefore does NOT treat any JSONL line as the turn boundary — eachruninvocation IS one turn, and the clean process exit is the boundary (synthesizeturn_complete); a failed exit with noerrorevent synthesizes anerror. A missing terminal event is harmless. - OpenCode tool events carry input + result in one line: a
tool_useevent'spart.stateholds bothinputand (whenstatus:'completed')output— so Neo emits both atool_useand atool_resultfrom one line, de-duped bycallIDacross any running→completed pair. - OpenCode session id is
sessionID(ses_...), top-level and insidepart. Prompt is passed via stdin (verified — safe for large briefs); resume via--session <ses_id>(verified, incl. together with stdin). - OpenCode does NOT honor the spawn cwd — Neo passes
--dir <cwd>(H4-C): live-verified thatopencode runresolves its project root from its own (SQLite-backed) state, NOT the working directory execa gives the child — even though execa sets the child cwd correctly (proven independently). Without--dira driven opencode session silently edits the WRONG repo (observed: it edited the parent checkout instead of the session cwd).opencode-cli.tstherefore always passes--dir <session cwd>;--dirprecedes--sessionin the argv. This is a safety-critical flag, not an optimization. - Codex emits token usage on
turn.completedbut no per-token USD (subscription-billed); it is logged, not surfaced ascostUsd. - Copilot
-pis per-turn + native resume: onecopilot -pinvocation runs a full agentic loop ending in a singleresultevent = one Neo turn. Multi-turn PINS the session id (--session-id=<uuid>on turn 1;--resume=<uuid>after) — the transport generates the uuid so the id is known before the first frame;result.sessionIdechoes it. Prompt rides argv (-p <prompt>), NOT stdin. - Copilot deny-wins:
--allow-all-toolsis required for non-interactive-p, and--deny-tooloverrides it (live-verified:--deny-tool=shellblocks the platform shell —powershell/bash— even under--allow-all-tools). The single ruleshellmatches the platform shell on any OS. - Copilot edit tool is
apply_patch(verified); each edit'stool.execution_completecarries a git-style unified diff inresult.detailedContent+ changed paths intoolTelemetry.restrictedProperties— consumed by Wave H3 diff-capture as a tool-event fallback. - Droid
-o jsonemits ONE final object ({"type":"result",…,"session_id"}) per invocation — no per-tool events, so diff capture leans entirely on the git path. A session-load failure (droid exec -s <missing-id>) exits non-zero with NO stdout at all; thecli-harness.tsexit-error synthesizer turns that into a propererrorevent. Prompt rides stdin (codex parity). - Windows
.cmd/.ps1shim spawning: all subprocess backends spawn throughcli-harness.ts(execa), which resolves npm-global shims correctly — directchild_process.spawnwould fail (Node CVE-2024-27980 mitigation). Grok is installed as a native.exe(not an npm shim), so this doesn't apply to it, but it still spawns through the samecli-harness.tstoolkit for consistency. - Grok headless
-pmode has NO approval channel — most--permission-modevalues silently CANCEL gated actions instead of asking (live-verified 2026-07-12,scratch/grok-live-capture.md): driving the identical "add a function to an existing file" task under--permission-mode acceptEditsand separately under--permission-mode dontAskBOTH returnedstopReason:"Cancelled"with the target file left UNCHANGED (and thetextfield sometimes empty) — the gated tool call was auto-DENIED, not auto-approved, because there is nothing to answer it in headless mode. Only--permission-mode autoand--permission-mode bypassPermissionscompleted the SAME task (stopReason:"EndTurn", file actually edited). Consequence:grok-cli.ts'sacceptEditsposture is mapped to grok'sautovalue, NOT grok's own confusingly-namedacceptEditsstring (fixed in this chip — the posture-mapping table above andgrokPermissionModeForPosturereflect this).default/planare left on their own literal grok values since Neo'sdefault/planpostures are intentionally cautious anyway (a plan/default session for grok should effectively no-op on gated actions) — just be aware it manifests as an emptyturn_completewith no file changes, not a blocked clarification.dontAskis NOT used by any Neo posture; its name is itself a trap (cancels, does not "skip asking and proceed"). - Grok's
--sandbox <PROFILE>flag is a SEPARATE lever from--permission-mode, live-verified as an OPEN string (not a closed clap enum — a bogus value reached a real API call rather than an argv-parse error), with documented valuesoff/workspace/read-only/strict"or a custom profile name". This transport does not set it (relies entirely on--permission-mode); a future chip could layer it in for defense-in-depth on untrusted-code review tasks. - Grok's
--toolsallow-list flag is documented but currently BROKEN (CR-R, 2026-07-12):grok --helplists--tools <TOOLS>as a real "built-in tools to allow" flag, but live re-verification against the installed grok 0.2.93 binary found that passing it with ANY value —bash,read, even the plausible-realrun_terminal_cmd— makes session creation fail every time with the same internal error (Requirements unsatisfied: [RequirementError { tool: "GrokBuild:run_terminal_cmd", message: "auto_background_on_timeout requires enabled_background to be true" ... }]), before any prompt is sent.--disallowed-toolsalone does NOT reproduce this.GROK_CAPS.honorsAllowedToolsisfalseas a result —code-sessionloud-rejects an explicitallowedToolsrequest forgrokrather than forwarding it to a flag that breaks the whole turn. Revisit once a future grok release fixes this (retest with the same probe:grok -p noop --output-format json --tools bash -m <bogus>, which fails fast without spending a real turn).