diff --git a/examples/rsc-agent-runtime/src/events/tool/after.tsx b/examples/rsc-agent-runtime/src/events/tool/after.tsx
index edfb8cbac..74e5e349b 100644
--- a/examples/rsc-agent-runtime/src/events/tool/after.tsx
+++ b/examples/rsc-agent-runtime/src/events/tool/after.tsx
@@ -4,6 +4,7 @@ import { Agent } from '@agent-bundle/runtime';
import type { AgentEventRouteProps } from 'agent-bundle';
import * as React from 'react';
+import { writeEvalProbe } from '../../hook/eval-probe.js';
import { normalizeClaudeHook, normalizeCodexHook } from '../../hook/normalize.js';
import { createFileRuntimeKernel, resolveImplicitRuntimeStateFile } from '../../runtime/state-file.js';
@@ -19,34 +20,40 @@ export default async function AfterFileEdit({
native,
signal,
}: AgentEventRouteProps) {
- const host = canonical.provenance.host;
- const normalized = host === 'claude'
- ? normalizeClaudeHook(native)
- : host === 'codex'
- ? normalizeCodexHook(native)
- : undefined;
- if (normalized === undefined) {
- throw new Error(`Unsupported event-route host ${JSON.stringify(host)}`);
- }
+ try {
+ const host = canonical.provenance.host;
+ const normalized = host === 'claude'
+ ? normalizeClaudeHook(native)
+ : host === 'codex'
+ ? normalizeCodexHook(native)
+ : undefined;
+ if (normalized === undefined) {
+ throw new Error(`Unsupported event-route host ${JSON.stringify(host)}`);
+ }
- const configuredStateFile = process.env.AGENT_RUNTIME_STATE_FILE;
- const stateFile = configuredStateFile === undefined || configuredStateFile.trim() === ''
- ? await resolveImplicitRuntimeStateFile(normalized.cwd)
- : resolve(configuredStateFile);
- const snapshot = await createFileRuntimeKernel({ stateFile }).recordEdit({
- host: normalized.host,
- idempotencyKey: canonical.idempotencyKey,
- path: normalized.path,
- sessionId: normalized.sessionId,
- toolName: normalized.toolName,
- }, { signal });
- const editNoun = snapshot.stateVersion === 1 ? 'edit' : 'edits';
+ const configuredStateFile = process.env.AGENT_RUNTIME_STATE_FILE;
+ const stateFile = configuredStateFile === undefined || configuredStateFile.trim() === ''
+ ? await resolveImplicitRuntimeStateFile(normalized.cwd)
+ : resolve(configuredStateFile);
+ const snapshot = await createFileRuntimeKernel({ stateFile }).recordEdit({
+ host: normalized.host,
+ idempotencyKey: canonical.idempotencyKey,
+ path: normalized.path,
+ sessionId: normalized.sessionId,
+ toolName: normalized.toolName,
+ }, { signal });
+ const editNoun = snapshot.stateVersion === 1 ? 'edit' : 'edits';
+ await writeEvalProbe(native, 0);
- return (
-
-
- {`Recorded ${basename(normalized.path)} from ${normalized.host}. Shared state now contains ${snapshot.stateVersion} ${editNoun}.`}
-
-
- );
+ return (
+
+
+ {`Recorded ${basename(normalized.path)} from ${normalized.host}. Shared state now contains ${snapshot.stateVersion} ${editNoun}.`}
+
+
+ );
+ } catch (error) {
+ await writeEvalProbe(native, 1).catch(() => undefined);
+ throw error;
+ }
}
diff --git a/examples/rsc-agent-runtime/src/hook/cli.ts b/examples/rsc-agent-runtime/src/hook/cli.ts
index c7e93dcb6..a0681b95a 100644
--- a/examples/rsc-agent-runtime/src/hook/cli.ts
+++ b/examples/rsc-agent-runtime/src/hook/cli.ts
@@ -1,40 +1,13 @@
-import { appendFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { requestAgentDocument } from '../flight/request-render.js';
import { resolveImplicitRuntimeStateFile } from '../runtime/state-file.js';
+import { writeEvalProbe } from './eval-probe.js';
import { normalizeClaudeHook, normalizeCodexHook } from './normalize.js';
import { projectHookDocument } from './project-document.js';
let probeInput: Record | undefined;
-const valueType = (value: unknown): string => {
- if (value === null) return 'null';
- if (Array.isArray(value)) return 'array';
- return typeof value;
-};
-
-const writeEvalProbe = async (input: Record, exitStatus: number): Promise => {
- const probeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE;
- if (probeFile === undefined || probeFile.trim() === '') return;
-
- const toolInput = input.tool_input;
- const toolInputRecord = toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput)
- ? toolInput as Record
- : undefined;
- const topLevelKeys = Object.keys(input).sort();
- const toolInputKeys = toolInputRecord === undefined ? [] : Object.keys(toolInputRecord).sort();
- await appendFile(probeFile, `${JSON.stringify({
- commandLaunched: true,
- exitStatus,
- toolInputKeys,
- toolInputValueTypes: Object.fromEntries(toolInputKeys.map((key) => [key, valueType(toolInputRecord?.[key])])),
- toolName: typeof input.tool_name === 'string' ? input.tool_name : undefined,
- topLevelKeys,
- topLevelValueTypes: Object.fromEntries(topLevelKeys.map((key) => [key, valueType(input[key])])),
- })}\n`);
-};
-
const readInput = async (): Promise> => {
let contents = '';
process.stdin.setEncoding('utf8');
diff --git a/examples/rsc-agent-runtime/src/hook/eval-probe.ts b/examples/rsc-agent-runtime/src/hook/eval-probe.ts
new file mode 100644
index 000000000..45fc1ec7d
--- /dev/null
+++ b/examples/rsc-agent-runtime/src/hook/eval-probe.ts
@@ -0,0 +1,28 @@
+import { appendFile } from 'node:fs/promises';
+
+const valueType = (value: unknown): string => {
+ if (value === null) return 'null';
+ if (Array.isArray(value)) return 'array';
+ return typeof value;
+};
+
+export const writeEvalProbe = async (input: Record, exitStatus: number): Promise => {
+ const probeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE;
+ if (probeFile === undefined || probeFile.trim() === '') return;
+
+ const toolInput = input.tool_input;
+ const toolInputRecord = toolInput !== null && typeof toolInput === 'object' && !Array.isArray(toolInput)
+ ? toolInput as Record
+ : undefined;
+ const topLevelKeys = Object.keys(input).sort();
+ const toolInputKeys = toolInputRecord === undefined ? [] : Object.keys(toolInputRecord).sort();
+ await appendFile(probeFile, `${JSON.stringify({
+ commandLaunched: true,
+ exitStatus,
+ toolInputKeys,
+ toolInputValueTypes: Object.fromEntries(toolInputKeys.map((key) => [key, valueType(toolInputRecord?.[key])])),
+ toolName: typeof input.tool_name === 'string' ? input.tool_name : undefined,
+ topLevelKeys,
+ topLevelValueTypes: Object.fromEntries(topLevelKeys.map((key) => [key, valueType(input[key])])),
+ })}\n`);
+};
diff --git a/examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts b/examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts
index ffa5040bc..021a3d5e0 100644
--- a/examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts
+++ b/examples/rsc-agent-runtime/tests/route-unit/event-route.test.ts
@@ -17,16 +17,20 @@ const fixture = resolve(import.meta.dirname, '../fixtures/events/claude-post-too
let workspace: string;
let previousStateFile: string | undefined;
+let previousProbeFile: string | undefined;
beforeEach(async () => {
workspace = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-event-route-'));
previousStateFile = process.env.AGENT_RUNTIME_STATE_FILE;
+ previousProbeFile = process.env.AGENT_RUNTIME_HOOK_PROBE_FILE;
process.env.AGENT_RUNTIME_STATE_FILE = join(workspace, 'state.json');
});
afterEach(async () => {
if (previousStateFile === undefined) delete process.env.AGENT_RUNTIME_STATE_FILE;
else process.env.AGENT_RUNTIME_STATE_FILE = previousStateFile;
+ if (previousProbeFile === undefined) delete process.env.AGENT_RUNTIME_HOOK_PROBE_FILE;
+ else process.env.AGENT_RUNTIME_HOOK_PROBE_FILE = previousProbeFile;
await rm(workspace, { force: true, recursive: true });
});
@@ -66,3 +70,48 @@ it('renders a native Claude PostToolUse envelope into the document the host proj
.toContainContext('Recorded claude-note.txt from claude. Shared state now contains 1 edit.');
expect(rendered.provenance).toMatchObject({ kind: 'event-route', proofLevel: 'route-unit' });
});
+
+it('appends a value-free eval hook probe when AGENT_RUNTIME_HOOK_PROBE_FILE is set', async () => {
+ const probeFile = join(workspace, 'hook-probe.jsonl');
+ process.env.AGENT_RUNTIME_HOOK_PROBE_FILE = probeFile;
+ const native = JSON.parse(await readFile(fixture, 'utf8')) as Record;
+
+ await renderRoute('event:tool/after', {
+ input: {
+ canonical: {
+ event: 'tool/after',
+ idempotencyKey: 'route-unit-claude-write',
+ observedAt: '2026-09-01T00:00:00.000Z',
+ provenance: {
+ host: 'claude',
+ hostContractRevision: 'route-unit',
+ nativeEvent: 'PostToolUse',
+ source: 'native',
+ },
+ sequence: 1,
+ },
+ native: { ...native, cwd: workspace },
+ },
+ });
+
+ const probe = JSON.parse(await readFile(probeFile, 'utf8'));
+ expect(probe).toEqual({
+ commandLaunched: true,
+ exitStatus: 0,
+ toolInputKeys: ['file_path'],
+ toolInputValueTypes: { file_path: 'string' },
+ toolName: 'Write',
+ topLevelKeys: ['cwd', 'hook_event_name', 'session_id', 'tool_input', 'tool_name', 'tool_response', 'tool_use_id', 'transcript_path'],
+ topLevelValueTypes: {
+ cwd: 'string',
+ hook_event_name: 'string',
+ session_id: 'string',
+ tool_input: 'object',
+ tool_name: 'string',
+ tool_response: 'object',
+ tool_use_id: 'string',
+ transcript_path: 'string',
+ },
+ });
+ expect(await readFile(probeFile, 'utf8')).not.toContain('claude-note.txt');
+});