Kiro IDE hook payload — empirical reference
How Kiro IDE delivers context to a command hook, captured live on TWO IDE
generations: 0.12-main (probe .kiro.hook files that dumped stdin, argv, and
the full environment) and 1.0.165 (probe v2 hook JSON files; upstream
543/#555). This is the evidence base for the harness/kiro-ide/ adapter; the
CLI harness (harness/kiro/) uses a different, kiro-cli-shaped stdin
mechanism.
The channel changed across IDE generations
| Kiro IDE 0.12 | Kiro IDE 1.x (≥1.0.1xx) | |
|---|---|---|
| Hook registration | .kiro/hooks/*.kiro.hook ({"version":"1.0.0","when":{...},"then":{...}}) |
.kiro/hooks/*.json v2 schema ({"version":"v1","hooks":[{name,trigger,matcher,action}]}, PascalCase triggers). Legacy .kiro.hook files are silently inert — never executed. |
| Context channel | USER_PROMPT env var (JSON string) |
stdin (JSON, written and closed). USER_PROMPT arrives empty. |
| stdin behavior | Opened but NEVER written/closed — a bare read hangs | Written and closed — a read resolves promptly |
| Field naming | camelCase: { toolName, toolArgs, toolResult, toolSuccess } |
snake_case: { session_id, hook_event_name, cwd, tool_name, tool_input, tool_response } — no success flag |
A live 1.0.165 PostToolUse capture, field-verbatim:
{"session_id":"sess_…","hook_event_name":"PostToolUse","cwd":"/path/to/project","tool_name":"execute_bash","tool_input":{},"tool_response":"Output:\n…\nExit Code: 0"}
The adapter uses a non-empty USER_PROMPT immediately (the 0.12 channel,
whose stdin never closes). When that variable is empty, it reads stdin for the
1.x channel, raced against a broken-channel timeout. The production default is
2s; a positive AIDLC_IDE_STDIN_TIMEOUT_MS value overrides the ceiling in
milliseconds for diagnostics and deterministic latency tests. Both field
spellings are accepted. Acquisition is gated to the three payload-dependent
targets (audit-and-sensors, log-subagent, rebuild-stage-graph) plus
session-start and continue-workflow for their modern session_id; every
other target (including the per-tool-call block floor) touches neither
channel and keeps its zero-latency path.
VSCODE_IPC_HOOK / VSCODE_PID are also present in the IDE (absent on the
CLI), but the adapter keys off the payload channels above.
Per-event captures
Result prose is identical on both channels (toolResult on 0.12,
tool_response on 1.x):
| Event | tool name | tool inputs | result prose | recoverable? |
|---|---|---|---|---|
| PostToolUse (write) — create | fs_write |
{} (empty) |
Created the <PATH> file. |
path: from the result prose only |
| PostToolUse (write) — edit | str_replace |
{} (empty) |
Replaced text in <PATH> |
path: from the result prose only |
| PostToolUse (write) — append | fs_append |
{} (empty) |
Appended the text to the <PATH> file. |
path: from the result prose only |
| PostToolUse (shell) | execute_bash |
{} (empty) |
Output:\n<stdout>\n\nExit Code: 0 |
command: not recoverable (only stdout) |
Critical limitations
- PostToolUse write/shell captures have empty tool inputs on both channels. Their written path must therefore be parsed from the result prose, and the shell command is absent (only stdout + exit code is present). This is not a universal IDE rule: later 1.x builds populate some PreToolUse inputs and delegation inputs (#543).
- 1.x carries no success flag. Only the 0.12 channel's explicit boolean
toolSuccess: falsedrops a well-formed write from the audit (#417); a 1.x payload with the field absent falls through to the path check. Because that channel cannot report failure structurally, a failed write on 1.x arrives only as error prose — so the adapter classifies before logging: prose RECOGNISED as a failure is sent tohookDebug(written only when hook debugging is enabled); there is no artifact to audit, so not forwarding it is correct, not decay. An unrecognised wording still records a visible hook-drop, which is the case that signals real degradation. On the legacy 0.12 channel, explicittoolSuccess: trueremains authoritative and bypasses failure-prose inference. A present non-null payload field with the wrong runtime type is treated as malformed: the advisory hook exits successfully, records a visible drop, and forwards no audit or subagent event.nullis treated like an unavailable field, matching the channel's existing absent-value contract. - Paths in the result prose are workspace-RELATIVE, but the core hooks compare against an absolute record root — so the adapter resolves them to absolute before forwarding.
Consequences for each hook
- write-audit-log / run-sensors — recoverable: scrape the file path from
the result prose, resolve to absolute, feed the core hooks the Claude-shaped
{tool_input:{file_path}}. When no path can be extracted the adapter splits two cases rather than logging both: prose recognised as a failed write is sent tohookDebug(written only when hook debugging is enabled) and is not forwarded because no artifact exists; this inference runs only when the payload has no structured success flag. ExplicittoolSuccess: trueand any other unmatched wording record a visible hook-drop (never a silent no-op) — that is the invisible-decay case the drop log exists to surface. Conflating them made--doctorreport degradation on healthy workspaces. - rebuild-stage-graph — the shell command is unrecoverable, so the IDE path
drops the command filter and gates purely on the audit tail (with an mtime
idempotency guard so a lingering transition — e.g. after
WORKFLOW_COMPLETED— does not recompile on every subsequent shell command). The shell result and session identity are still forwarded: modern events use their exactsession_id, while the legacy channel uses the synthetic identity retained by SessionStart. When the result names a successfulintent-create, the shared hook binds that session to the created record. - sync-workflow-state — the IDE gives no task payload, so it derives the current
stage from the latest
STAGE_STARTEDin the audit tail. This is a forward-only mirror: it never rewindsCurrent Stageto a completed or skipped stage, and never fires when the workflow is notRunning(guards against resurrecting a finished workflow). Matched toexecute_bash— the IDE surfaces no task event the sync could parse. - log-subagent — payload-dependent. IDE 0.12 sent
invoke_sub_agent; 1.x (1.0.89-1.0.138) sentsubagent_<agent>instead, each preceded by an emptysubagent_responseshell ("Response recorded."). The registration matcher is therefore broad (^(subagent_.+|invoke_sub_agent)$) so every delegate name reaches the adapter, and the adapter dropssubagent_response— that shell carries prose but no identity, so forwarding it would fabricate aSUBAGENT_COMPLETEDrow withAgent Type: unknown. Identity prefers the structured 1.xsubagent_<agent>tool name (#543) — it is platform-provided, so agent-authored result prose cannot misattribute the audit row — and falls back to the**Reviewer:**/**Agent:**result marker from #459, which is the only identity signal on the 0.12invoke_sub_agentshape. - session-start — reads the modern
session_idand persists it under the gitignored runtime session directory; the legacy channel records its stable synthetic ID instead. - stop — reads the modern Stop event's
session_idand prefers it over the workspace-global SessionStart marker, so concurrent chats consume only their own post-create handoff receipts. Legacy agentStop and broken modern channels fall back to the retained identity. - session-end / mint / block — need no payload and never read stdin. Session-end reuses the identity persisted by SessionStart, with the legacy synthetic ID as the fallback.
toolResult path-extraction patterns
| toolName | wording | canonical tool |
|---|---|---|
fs_write |
Created the <PATH> file. |
Write |
str_replace |
Replaced text in <PATH> (may carry a trailing (N occurrences)) |
Edit |
fs_append |
Appended the text to the <PATH> file. |
Edit |
The extractor trims trailing whitespace/newlines before matching and strips a
trailing parenthetical from the str_replace form. fs_write maps to Write;
str_replace/fs_append map to Edit (both target an existing file → the core
write-audit-log records ARTIFACT_UPDATED).