A probing plugin. Install it into a Claude Code, Codex, or Cursor home, drive
one agent session, and read back exactly what that host sent to every plugin
hook and MCP call — the raw envelope, the framework request context each
handler saw, and the conversation lineage the runtime resolved for it.
It is the acceptance vehicle for request.lineage and the evidence source for
docs/audits/*-host-lineage-matrix.md.
From the repository root, launch the Workbench with:
pnpm example:host-testEvery canonical event family the framework admits has a semantic event route
under src/events/**, each restricted to the hosts whose pinned capability
table supports it. Every route appends one NDJSON line and dispatches a
bounded summary into the durable state kernel (src/state.ts,
host-test/captures, workspace-durable). A line carries:
| Field | Contents |
|---|---|
event.native |
The complete host payload, byte for byte, with secret-looking values replaced by [redacted]. |
event.canonical |
The framework's canonical identity (event, idempotencyKey, observedAt, provenance). |
request |
(await agent()) as the route saw it: invocation, host, session, actor, workspace, capabilities, lineage, provider keys, and whether state and notices were mounted. lineage is always present: available with the resolved tree position, or unavailable with the runtime's per-host reason. |
ids |
Every identity-shaped native field (conversation_id, generation_id, session_id, subagent_id, tool_call_id, agent_id, turn_id, user_email, …) lifted out for filtering. |
process |
pid, ppid, cwd, execPath, entry file, uptime — of the process that ran the route. |
runtime |
shared-runtime when the hook reached the warm MCP-hosted runtime, standalone-hook when it fell back to the hook process, mcp-server, or cli. |
env.names |
Environment variable names matching CURSOR_*, CLAUDE_*, CODEX_*, AGENT_BUNDLE_*, PLUGIN_*, MCP_*, HOST_TEST_*. Values are never written. |
Two MCP servers ship in the plugin:
host-test(generated routes,src/mcp/host-test/tools/):dump(filter by any conversation/session/subagent id,fullfor raw lines),reset, andslow. Eachdumpcall records the request context the generated server mounted for it. A baredumpreturns the newest 50 matching records — a whole log of a few hundred records overflows the tool-result document — so passlimit(up to 5000) for more;matchedandtotalalways count the whole log.slowholds a call open forholdMs(up to 30 s) and reports progress everytickMs; it declaresexecution.taskSupport: "optional", so a host that speaks the MCP2025-11-25Tasks utility may run it as a task (tools/callanswered by a task handle, the result throughtasks/result) while every other host receives the ordinary result. The recorded call and thehost-test-rawenvelope show which path the host took.host-test-raw(hand-rolled stdio factory,src/mcp/host-test-raw.ts):proberecords the raw SDK request context — session id, JSON-RPC id,_meta, lifted envelope, negotiated client info — so hook↔MCP correlation is judged against the wire.
The rendered CLI host-test dump [--conversation <id>] [--full] [--log <file>]
(dist/bin/host-test.mjs) reads the same log outside any host.
The log lives at $HOST_TEST_LOG_DIR/captures.ndjson when that variable is
set, otherwise $AGENT_BUNDLE_PLUGIN_ROOT/state/host-test/captures.ndjson
(the installed plugin root the host hands its MCP servers), otherwise beside
the artifact that ran the hook, otherwise ~/.host-test/. dump always
prints the path it used.
Everything runs in an isolated home under /tmp/host-test/<host>-home
(override with HOST_TEST_ROOT); the real ~/.claude, ~/.codex, and
~/.cursor are never opened or written.
pnpm --filter @agent-bundle-example/host-test build
pnpm --filter @agent-bundle-example/host-test probe:install claude # or codex | cursor
pnpm --filter @agent-bundle-example/host-test probe:capture claude # one scripted session
pnpm --filter @agent-bundle-example/host-test probe:status claude
pnpm --filter @agent-bundle-example/host-test probe:uninstall claudeprobe:installbuilds when needed, creates the isolated home plus a scratch git workspace, copies the host's sign-in file byte-for-byte into the isolated home (--no-authskips it; the copy is removed byprobe:uninstall), and runsagent-bundle install <host> --from artifactwithHOME,CLAUDE_CONFIG_DIR, orCODEX_HOMEpointed at the isolated home. For Cursor it prints the isolated IDE launch line (--user-data-dir,--extensions-dir).probe:captureruns the scenario prompt throughclaude -p,codex exec, orcursor-agent -p: a shell command, a file edit,dump,probe, one subagent that repeats those and tries a nested subagent, thenHOST_TEST_DONE. The session transcript and the records this run appended tocaptures.ndjsonland in/tmp/host-test/<host>/, followed by a renderedhost-test dump. Earlier runs' records are never re-copied, and the command exits non-zero when the host fails or when the run produced no hook record or no MCP record.probe:capture <host> --scenario <file.json>replaces the default prompt with an ordered list of turns ({ "turns": ["...", "..."] }; a turn may also be{ "prompt": "..." }). For Claude every turn after the first runsclaude -p --resume <session_id>against the session the first turn'ssystem/initenvelope reported, so one capture holds a multi-turn session withSessionStart source: resumeand oneSessionEndper invocation.Stopfollows the prompt path rather than the invocation: a turn whose background subagents finish re-prompts itself with a<task-notification>and stops twice, while a/compactturn submits no prompt and never stops (fixtures/host-lineage/claude-2.1.259-orchestration.ndjson). If the first turn reports nosession_id, the capture fails instead of running the remaining turns as fresh sessions;--promptand--scenariorefuse each other, and--scripted-modelplays a fixed transcript and refuses both. Codex and Cursor drivers take the first turn only and refuse longer scenarios.scenarios/claude-orchestration.jsonis the checked-in orchestration scenario (two parallelAgentspawns, one sequential spawn that nests another, thehost-test:host-testskill, a plugin-command probe, a manual/compact, a final stop).- Claude turns run with
--output-format stream-json --verbose, so the model's own tool-use stream is saved next to the hook payloads assession-<stamp>[.turn-N].stream.ndjson; envelopes produced inside a subagent carryparent_tool_use_id(the parent'sAgenttool_use_id), which is how the transcript and the hook log are cross-checked. - Host processes get an allowlisted environment (PATH, locale, display, proxy,
TLS plumbing) plus the isolated
HOME; nothing else from your shell is inherited, and even allowlisted values are dropped when they carry a credential (proxy URLs with userinfo, bearer tokens). Hosts authenticate only from the copied sign-in files. probe:uninstallruns the host's own uninstall (claude plugin uninstall,codex plugin remove, or removing~/.cursor/plugins/local/host-test) and deletes the isolated home. Captures under/tmp/host-test/<host>/survive.
Cursor IDE sessions cannot be driven by -p; launch the isolated instance with
the printed command, open the Agents pane, and use the same scenario prompt.
- Application lists the twenty event routes, both MCP servers, the skill,
and the routed CLI with their per-target capability judgments —
workspace/openis Cursor-only,task/*andfile/changeare Claude-only, and portable carries no hooks at all. - Under Application → Events / Hooks, select any family and run it with
canonical input; the route appends a record to the log and returns an empty
result (only
session/startspeaks anadditional_contextline naming the log path). - Run the
host-test dumpCLI leaf or the MCPdumpTool leaf against the same log, so a simulated hook is visible from the MCP surface immediately. - Use the selected event route's Replay tab for checked-in native receipts; each replay shows its request context and lineage.
cd examples/host-test
pnpm validate
pnpm build
pnpm typecheck
pnpm testpnpm check runs the four in order.