Skip to content

shim-runtime

The ONE shared runtime embedded into every generative per-session shim (pi's extension, opencode's plugin) — claude-shaped dispatch, the single block-verdict contract (exit 2 + stdout decision:block JSON), and the multi-connection rendezvous server. A new harness generator declares only its event mapping and host API bindings.

Some harnesses have no external hook binding: their shim is GENERATED code running inside the agent process (pi's extension, opencode's plugin — pi-harness, opencode-harness). The first two generators each hand-wrote the same machinery, and the divergence cost was measured, not hypothetical: the deliver delivery-confirm bug was fixed only in opencode's copy, the dropped stop-gate rejection only in pi's. This node is the cure: ONE embedded runtime source every generative shim composes, so a generator declares nothing but its event-name mapping and its host API bindings — the socket protocol and the verdict parse are never rewritten. That is the acceptance test for any future harness of this kind.

The runtime owns five shared contracts:

  • dispatch — synthesize the claude-shaped payload (session_id, cwd, hook_event_name, extras) and pipe it to dispatch.sh <harnessId> <Event>, the id baked as argv[1] (the shell-side detector), returning the exit code plus both streams. Hosts keep ordering tool payloads so agent_id precedes tool_input (the harness.sh prefix-scan contract).
  • the ONE block verdict — blocked = exit 2, nothing else. The reason is read in contract order: a strict decision:block JSON line on stdout (the designed channel), then a regex over GLUED handler outputs (several handlers' stdout concatenates without separators), then stderr (a bare exit-2 handler), then the caller's fallback — always the parsed human-readable reason, never the escaped wire JSON. codex's stderr bridge is codex's own native protocol and stays in dispatch.sh, untouched here.
  • the rendezvous server — binds the launch-injected rendezvous socket and accepts reply pokes, so the shared Claude-family adapter and socket-listener liveness are reused verbatim. MULTI-connection means a board probe cannot displace a concurrent poke. A repeated mid is injected once for this live shim; an injection error releases that marker, while an unable host leaves the message owed for the queue to retry. It never confirms or rejects an injection: the timeline is the delivery.
  • the stop-gate loop closure (dispatchStop) — a blocked Stop's continuation is a LOOP, and the loop needs a termination bit: claude's native Stop payload carries stop_hook_active=true inside a hook-forced continuation, and the stop-gate's escape paths (auto-declare / downgrade-to-ask) key on exactly that bit to guarantee the loop ends. A generative host has no native bit, so the runtime supplies it — false on a natural stop, true on the settle that follows a blocked one, reset by the first allowed stop — and on block re-enters the gate's parsed reason through the host's inject. Without the bit every settle reads as a FIRST stop, a gate that can never pass (the commit gate on a 0-commit worktree) blocks forever. An inject the host can no longer take is caught LOUD on stderr, never thrown into the host and never silent.
  • tool-input normalization — the host's file-path spelling (pi path, opencode filePath) is moved onto claude's file_path, the key every claude-family handler reads.

The chunk is plain untyped ESM over node builtins — the lowest common denominator of pi's native-.ts loader, opencode's plugin loader, and a bare node import in tests — and is embedded VERBATIM by each generator: the generated file stays self-contained (nothing to resolve from a worktree at run time), and a materialize refreshes every clone from this one source.