Skip to content

Harness capture matrix

Reproduced from docs/harness-capture-capability.md (the canonical source) — edit that file, not this page.

Automatic capture (spec 2026-06-16-harness-auto-capture, ADR 0009) feeds a uniform server contract — the POST /transcript delta endpoint — through a thin per-harness acquisition adapter. The server pipeline (buffer → settle-sweep → extractor → curator) is built once and shared; only the small adapter that acquires a per-turn delta differs per harness. This page is the honest, seeded status of each harness (spec SC14), grounded in the §11.2 capability audit of the capture brainstorm and the §6 live test, and re-grounded in Phase 2A against mem0’s shipping plugin — which proves Codex (same hook events as Claude) and OpenCode (chat.message), correcting the earlier “Codex blocked / OpenCode idle-bracketing” framing.

The three columns that decide whether a harness can capture at all:

  • Capture mechanism — how the adapter gets a per-turn delta.
  • conv_id stability — capture keys all per-conversation state by a stable conversation id (never $USER or cwd, spec §4.11). A harness without a stable id can’t attribute deltas safely and is blocked.
  • Status — whether Phase 1 ships it, and what gates the rest.
Harness Capture mechanism conv_id stability Status
Claude Code UserPromptSubmit hook (primary) → tail the top-level transcript_path JSONL from a byte-offset cursor (subagents skipped; private turns skipped); Stop / SessionEnd kept as supplementary stablesession_id; concurrent sessions write distinct <session_id>.jsonl files (§6) Authoritative (shipped). Driven by UserPromptSubmit because Claude bug #29767 means plugin-scoped Stop hooks register but never fire; Stop / SessionEnd stay wired so capture auto-recovers when the bug is fixed. Shipped in integrations/claude/hooks/hooks.json.
Claude Cowork (desktop) None new — Cowork shares Claude Code’s plugin host, so the same integrations/claude/ hooks (on-stop.mjs) are the acquisition surface inherits Claude’s session_id (assumed, unverified on the desktop host) Blocked on verification (Phase 2A). No new adapter — the same Claude plugin is the surface. Needs the Claude desktop app to confirm its plugin host fires the per-turn UserPromptSubmit / Stop / SessionEnd hooks with a usable transcript_path + session id (and whether #29767 manifests there too). GUI install (Customize → Browse plugins) is documented in the Claude README; capture is fail-soft if the desktop host diverges.
Codex UserPromptSubmit hook (primary) → reuse the Claude adapter (on-stop.mjs + lib/*) on the per-turn delta; Stop / SessionEnd supplementary. Installed by merging into ~/.codex/hooks.json (owner-marker idempotent; requires codex_hooks = true in config.toml) assumed stablesession_id → transcript-filename basename → clean no-op; never cwd / $USER Ported (Phase 2A), e2e UNVERIFIED. Built optimistically from mem0’s shipping Codex hooks (install_codex_hooks.py, codex-hooks.json), which use the same hook events as Claude. The earlier “blocked: no stable id” status is resolved iff the live Codex payload carries transcript_path + a session_idnot yet confirmed (no codex CLI to spike). deriveConvId degrades gracefully and the parser is fail-soft, so a wrong payload shape no-ops rather than mis-attributing.
OpenCode chat.message plugin hook (TS, @opencode-ai/plugin) → build the per-turn delta from the full ordered message list (session.messages, both roles); fires on the user message, so the assistant reply lands one turn behind (the §8.2 tolerance) stablesessionID (confirmed in the v1.16.2 API) Ported (Phase 2A), e2e pending. Real TS plugin modeled on mem0’s opencode-mem0.ts, but keyed by sessionID (not mem0’s $USER — the collision bug we avoid). Unit + live-local-server contract verified; live OpenCode e2e pending (no opencode CLI). Corrects the earlier session.idle-bracketing guess.
Pi agent_end event → completed AgentMessage[] in-payload (O(1), no cursor; prose blocks only) stablectx.sessionManager.getSessionId() Ported (Phase 2B), e2e pending. TS extension hook confirmed against @earendil-works/pi-coding-agent@0.75.5 types (agent_end fires once per prompt with the turn in-payload; getSessionId() stable). The agent_end (whole-prompt) vs turn_end (per-response) choice is the one optimistic assumption — fail-safe (an unexpected shape → clean no-op). Live Pi e2e pending (no pi CLI to spike).
Hermes sync_turn(user, assistant, *, session_id, messages) → both halves handed in as args in-payload (O(1), no cursor) stablesession_id ({ts}_{shortuuid}) Ported (Phase 2B) — spike RUNTIME-CONFIRMED. The installed Hermes agent v0.16.0 still fires sync_turn per completed turn (turn_finalizerMemoryManager.sync_all, confirmed live via a throwaway provider). Python adapter posts the delta with exchange-granular private skip. Best-verified of Phase 2 alongside Claude; the only deferred check is a live server socket round-trip.

Claude Code is the harness the owner uses daily, so Phase 1 builds and dogfoods it. The §6 live test confirmed the data layer directly: the transcript is clean append-only JSONL (so a byte-offset cursor is valid), each entry carries a stable sessionId, concurrent sessions write distinct files, subagent work is isolated in separate subagents/*.jsonl, and cwd can change within a session — which is exactly why the buffer is keyed by conv_id, not cwd.

Capture is driven by UserPromptSubmit, not Stop. Claude bug #29767 is that plugin-scoped Stop hooks register but never fire (a SessionStart from the same plugin does fire), so a Stop-only adapter would silently never run. UserPromptSubmit fires reliably and carries the same session_id + transcript_path, so the adapter reads the same per-turn delta — one turn behind (it fires just before the assistant reply), which spec §8.2 already tolerates. The Stop / SessionEnd entries stay wired as supplementary so capture auto-recovers the moment the bug is fixed; the cursor’s advance-on-ack makes multiple firing events idempotent.

These are contract-level, not per-harness:

  • Default-on, gated two ways (see the slash-command / private-mode contract): the per-machine LIBRARIAN_AUTO_SAVE=false kill-switch and per-turn private-mode skip ([librarian:private=on]).
  • Server-authoritative intake gate. Even with the client shipping, the server buffers only when its curator intake gate (curator.intake.enabled) is on; if off it refuses and buffers nothing (no raw text at rest for a dead pipeline). The Claude SessionStart banner surfaces both gate states.
  • Fail-soft. A capture/guard/extraction error never blocks the user’s turn, never leaks a stack trace into the model’s context, and errs toward not capturing on any uncertainty.

See the Phase-1 spec for the full success criteria and the Claude integration README for the shipped hooks.