Skip to content

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

BackendidProcess modelGranularityPer-tool gating (honorsAllowedTools)Mid-turn clarifyResumeModel selectionCostLive: interrupt / set-posture / guide-mid-turn
Claude Codeclaude-codepersistent SDK query()stream (per-model-turn)✅ yes (SDK allowedTools)✅ yes (canUseTool)nativeper-turn (query.setModel)subscription✅ / ✅ / ❌
OpenAI Codexcodexper-turn codex exec subprocessper-turn❌ no (sandbox is the boundary)❌ nothread-id (resume <id>)per-turn (fresh argv/turn)subscription✅ / ✅ / ❌
Neo internalneoin-process agent-loop roundper-turn✅ yes (intersected)✅ yes (ask-parent bridged)noneper-session (same provider — Wave J F1)internal✅ / ✅ / ❌
OpenCodeopencodeper-turn opencode run subprocessper-turn❌ no (posture is the boundary)❌ nonative (--session <ses_id>)per-sessionapi-metered✅ / ✅ / ❌
GitHub Copilotcopilotper-turn copilot -p subprocessper-turn✅ yes (--available-tools)❌ no (-p one-shot)native (--session-id/--resume)per-sessionsubscription✅ / ✅ / ❌
Factory Droiddroidper-turn droid exec -o json subprocessper-turn❌ no (--auto tier is the boundary)❌ nonative (-s <session_id>)per-sessionapi-metered✅ / ✅ / ❌
xAI Grok Buildgrokper-turn grok -p --output-format json subprocessper-turn❌ no (--tools live-verified to crash session creation, CR-R)❌ no (-p one-shot)native (-s/--resume/--continue)per-sessionapi-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:

BackendeditToolNamesFallback when git is unavailable
claude-codeWrite, Edit, MultiEdit, NotebookEditfile list from tool events
codexapply_patchfile list from tool events (file_change items normalize to apply_patch)
neowrite-file, edit-filefile list from tool events
opencodewrite, edit, patchfile list from tool events
copilotapply_patch, writefile list from tool events (each edit's execution_complete also carries a unified diff)
droid(none)git path onlydroid 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 postureclaude-code (SDK mode)codex (--sandbox)neo (PermissionMode)opencode (run flags)copilot (-p flags)droid (--auto)grok (--permission-mode)
planplanread-onlyplan--agent plan (read-only agent)--mode plan --allow-all-tools (read-only planner)(no --auto — read-only)plan
defaultdefaultworkspace-writedefault(no flag — build agent, permissive)--allow-all-tools --deny-tool=shell --deny-tool=write --deny-tool=apply_patch (read/search only)--auto lowdefault
acceptEditsacceptEditsworkspace-writeacceptEdits(no flag — same as default)--allow-all-tools --deny-tool=shell (edits auto-approved, shell denied)--auto mediumauto
fullbypassPermissionsdanger-full-accessbypass--auto (auto-approve non-denied)--allow-all--auto highbypassPermissions

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/acceptEdits are NOT "ask first". They defer to opencode's own permission config, which for a stock install is permissive (*:allow on the built-in build agent) — edits proceed without a hang. A user's own opencode.json deny rules still apply. Treat default here as "let it edit", not "prompt me".
  • A per-session temp opencode.json was considered (to synthesize an ask/acceptEdits gradient) and rejected: run has no --config flag, and writing opencode.json into the session cwd would pollute the user's repo and race concurrent sessions. Finer per-tool control lives in opencode 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 sentinels cheap / standard / frontier (ModelTier, providers/model-router.ts). A sentinel is resolved at start time to the backend's concrete id via its descriptor modelClassMap (capabilities.ts); a sentinel with no mapping (a null/absent entry — the copilot auto-model precedent) or backend neo (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).
  • effortlow / medium / high / max (CC-parity naming). Accepted only by a backend whose descriptor effortSupport names a live-verified mechanism; an explicit effort against a backend with 'none'/'unknown' (or the field absent) is REJECTED loudly at the tool layer, the same posture as the allowedTools/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.

BackendMechanismlow / medium / high / maxStatus
neointernal thinking plumbing → CompletionRequest.thinking:{type:'enabled',effort} (claude-provider.ts)low / medium / high / maxhigh (clamped)KNOWN — pure internal plumbing, live
claude-codeSDK 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 / maxWIRED (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→xhighWIRED (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 setModelmodelSelection 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_KEY needed).

OpenAI Codex (codex)

  • Install: npm i -g @openai/codex.
  • Auth: codex login. Neo reuses ~/.codex/auth.json read-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/.ps1 shim through the cli-harness.ts execa 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 --version succeeds even with NO provider credential configured, so list-backends reports available:true; an auth/credential failure surfaces at run time as an error event (error.data.message) + non-zero exit, normalized to a CodeAgentEventerror. Same posture as codex.

GitHub Copilot (copilot)

  • Install: npm install -g @github/copilot (Node 22+). Docs say Windows wants PowerShell 6+, but Neo spawns the copilot shim through the cli-harness.ts execa toolkit and it works from a stock shell — no pwsh install needed for our spawn path.
  • Auth: /login device flow, or a token in COPILOT_GITHUB_TOKEN > GH_TOKEN

    GITHUB_TOKEN (a fine-grained PAT needs the "Copilot Requests" permission). copilot --version does NOT check auth, so list-backends shows available:true even un-authed; an auth failure surfaces at run time as an error event + non-zero exit. Same posture as codex/opencode.

Factory Droid (droid)

  • Install: npm i -g droid (the npm droid package IS Factory's CLI — verified v0.169.0) or the PowerShell installer irm https://app.factory.ai/cli/windows | iex.
  • Auth: FACTORY_API_KEY=fk-... (app.factory.ai/settings/api-keys) or the interactive /login device flow. Neo does NOT persist the key; it inherits the ambient env. droid --version succeeds regardless of auth, so list-backends shows available:true; an unauthenticated run surfaces as an error event 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 via customModels in ~/.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 as custom:<model-id> (via -m/--model, already how buildDroidArgs forwards o.model in droid-cli.ts) routes the ENTIRE turn to the BYOK provider — live-verified end-to-end against Novita (tencent/hy3) with bothFACTORY_API_KEY unset and ~/.factory/auth.v2.file/auth.v2.key temporarily removed (zero Factory account state of any kind): droid exec --model custom:tencent/hy3 -o json --auto low "..." returned a clean type:"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-8 etc.) — a BYOK custom model needs neither. No code change was needed in droid-cli.ts: probeAvailability never gated on FACTORY_API_KEY (only droid --version, which already succeeds unauthenticated), and buildDroidArgs already forwards an arbitrary o.model string opaquely to -m, so custom:<id> was already wireable.
  • Mode: per-invocation droid exec -o json (one final type:"result" object per turn). stream-jsonrpc is 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) or curl -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-auth for headless/remote), or an XAI_API_KEY env 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) — see GROK_CAPS.costProfile's note in grok-cli.ts for 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 --version does NOT require auth (prints the version regardless), so list-backends reports available:true even fully unauthenticated — same posture as codex/opencode/copilot/droid. An auth failure surfaces at run time as a type:"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> against grok-4.5 returned exactly {"text":"PONG97531","stopReason":"EndTurn","sessionId":"<uuid>", "requestId":"<uuid>","thought":"…"}no type field at all (unlike the error envelope, which always has type:"error"). A second turn via --resume <uuid> confirmed the same shape, sessionId unchanged. The earlier-captured error shape: supplying a syntactically-valid but WRONG XAI_API_KEY to grok -p "hi" --output-format json returns exactly {"type":"error","message":"Internal error: {...API error (status 400 Bad Request): invalid-argument: Incorrect API key provided...}"} — normalized 1:1 in normalizeGrokEvent. Since the success envelope never carries a type field, ev.type === 'error' is a clean, unambiguous discriminator between the two live-verified shapes. grok-cli.ts extracts response text from the (now-verified-first) text field, with result/response/ content/message/thought kept as forward-compatible fallbacks for a future CLI release. The turn boundary still does not gate on recognizing a field: like opencode (chosen there for terminal-event flakiness, #26855; chosen here because --output-format json prints exactly one terminal object with no earlier "more to come" marker), turn_complete is synthesized from the clean process exit — this keeps the transport robust to a future schema change without another live-capture chip.
  • CRITICAL: --output-format json is 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 supervised code-session through the grok backend 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's turn_complete events all carried text:"" despite the file actually being edited correctly, per the git diff). grok-cli.ts now buffers each turn's lines and parses the REJOINED whole-turn text exactly once, after the process closes stdout — safe because --output-format json always 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/--continue instead"), then --resume <uuid> on every later turn. Live-verified: the success envelope's own sessionId field 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, so cheap/standard resolve to "omit the flag").
  • Effort probe caveat (own finding): running the free --reasoning-effort argv-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 run can exit before emitting its final step_finish{reason:'stop'} and may never emit the user prompt message. Neo therefore does NOT treat any JSONL line as the turn boundary — each run invocation IS one turn, and the clean process exit is the boundary (synthesize turn_complete); a failed exit with no error event synthesizes an error. A missing terminal event is harmless.
  • OpenCode tool events carry input + result in one line: a tool_use event's part.state holds both input and (when status:'completed') output — so Neo emits both a tool_use and a tool_result from one line, de-duped by callID across any running→completed pair.
  • OpenCode session id is sessionID (ses_...), top-level and inside part. 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 that opencode run resolves 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 --dir a driven opencode session silently edits the WRONG repo (observed: it edited the parent checkout instead of the session cwd). opencode-cli.ts therefore always passes --dir <session cwd>; --dir precedes --session in the argv. This is a safety-critical flag, not an optimization.
  • Codex emits token usage on turn.completed but no per-token USD (subscription-billed); it is logged, not surfaced as costUsd.
  • Copilot -p is per-turn + native resume: one copilot -p invocation runs a full agentic loop ending in a single result event = 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.sessionId echoes it. Prompt rides argv (-p <prompt>), NOT stdin.
  • Copilot deny-wins: --allow-all-tools is required for non-interactive -p, and --deny-tool overrides it (live-verified: --deny-tool=shell blocks the platform shell — powershell/bash — even under --allow-all-tools). The single rule shell matches the platform shell on any OS.
  • Copilot edit tool is apply_patch (verified); each edit's tool.execution_complete carries a git-style unified diff in result.detailedContent + changed paths in toolTelemetry.restrictedProperties — consumed by Wave H3 diff-capture as a tool-event fallback.
  • Droid -o json emits 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; the cli-harness.ts exit-error synthesizer turns that into a proper error event. Prompt rides stdin (codex parity).
  • Windows .cmd/.ps1 shim spawning: all subprocess backends spawn through cli-harness.ts (execa), which resolves npm-global shims correctly — direct child_process.spawn would 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 same cli-harness.ts toolkit for consistency.
  • Grok headless -p mode has NO approval channel — most --permission-mode values 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 acceptEdits and separately under --permission-mode dontAsk BOTH returned stopReason:"Cancelled" with the target file left UNCHANGED (and the text field 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 auto and --permission-mode bypassPermissions completed the SAME task (stopReason:"EndTurn", file actually edited). Consequence: grok-cli.ts's acceptEdits posture is mapped to grok's auto value, NOT grok's own confusingly-named acceptEdits string (fixed in this chip — the posture-mapping table above and grokPermissionModeForPosture reflect this). default/plan are left on their own literal grok values since Neo's default/plan postures are intentionally cautious anyway (a plan/default session for grok should effectively no-op on gated actions) — just be aware it manifests as an empty turn_complete with no file changes, not a blocked clarification. dontAsk is 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 values off/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 --tools allow-list flag is documented but currently BROKEN (CR-R, 2026-07-12): grok --help lists --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-real run_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-tools alone does NOT reproduce this. GROK_CAPS.honorsAllowedTools is false as a result — code-session loud-rejects an explicit allowedTools request for grok rather 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).