Kiro IDE hook payload — empirical reference
How Kiro IDE delivers context to a command hook, captured live on 0.12-main
(probe .kiro.hook files that dumped stdin, argv, and the full environment),
1.0.165 (probe v2 hook JSON files; upstream #543/#555), and 1.0.242
(UserPromptSubmit and PreToolUse probes on Windows). 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 redacted native Windows before/after captures are retained in
research/kiro-windows-output-encoding/.
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 payload-dependent targets,
including plan-approval-guard, the two terminal-command targets, plus
session-start and continue-workflow for their modern session_id, and
record-human-turn for the exact approval response. Every other target
(including the per-tool-call approval floor) touches neither channel and keeps
its zero-latency path.
The legacy environment variable name does not imply raw user text: the measured
0.12 contract is camelCase JSON. A promptSubmit payload without a prompt
field therefore advances only the legacy turn clock; the terminal utility is
recognized later from toolArgs.command on the matching preToolUse event. Raw
/aidlc ... text remains accepted for newer Kiro generations that expose it
directly, but is not the 0.12 compatibility claim.
VSCODE_IPC_HOOK / VSCODE_PID are also present in the IDE (absent on the
CLI). Legacy Plan Approval hashes those measured host-instance values into its
runtime session identity, so two IDE windows in one workspace do not share
challenge/response files. Other adapter routing still 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? |
|---|---|---|---|---|
| UserPromptSubmit (1.0.242) | n/a | {prompt:""} |
n/a | prompt: no; session id: yes |
| PreToolUse (shell, 1.0.242) | execute_pwsh |
{command,cwd,run_in_background,timeout} |
n/a | command: yes |
| 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, and delivery is not uniform across generations:
later 1.x builds populate some PreToolUse and delegation inputs (#543).
Issue #763 reports that Kiro IDE 1.0.309 populated PreToolUse subagent
dispatch with
promptandexplanation, shell/write matchers withcommand,cwd,run_in_background, andtimeout, and PostToolUse inputs as well. That 1.0.309 observation was reported, not measured in this repository; the measured base is the 0.12, 1.0.165, and 1.0.242 captures described above. - 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 host-derived 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. - plan-approval-guard — populated PreToolUse arguments are forwarded to the
shared target-aware guard. Kiro IDE 0.12 identifies the tool but supplies an
empty argument object, so the adapter uses a mediated source-floor protocol:
only the measured
fs_writeandstr_replacetools remain available while planning; shell, append, delete, patch, aliases, and custom mutation tools stop before approval. After a canonical plan write the adapter injects the current Testing Contract. After a canonical questions write it replaces the target-bound fingerprint and invokes the reserved decision or answer tool itself. Kiro discards PostToolUse stdout, so that write hook remains silent. The invokingnextor final steeringcontinuecarries onelegacy_plan_approval_choicescapability in its Code Generation directive. Runtime stores only its hashes, while the plaintext labels remain in that chat's tool result. The human's laterpromptSubmitmust carry one exact label; unrelated prompts receive no capability. Ownership is global for the active intent/revision rather than looked up through the incoming session. Recovery is human-gated: the owning window first receives a typed recovery ask and must record the exactRecover Plan Approvalresponse before a laternextrotates any offer or challenge. Another live window is refused. A replacement window can request the same human recovery only after the recorded owner PID is gone, or after an IPC-only owner's endpoint disappears. Takeover rotates any pending challenge and clears its prior response. The shared questions file stays canonical and the audit redacts the choices. Missing or corrupt offer/challenge files do not make authority disappear: the pre-write window itself is an orphan-recovery latch if PostToolUse never arrives, and a matching write violation continues that latch when PostToolUse does run. Fresh publication remains blocked until the exact human recovery response. The adapter preserves a recovery ask and, after success, clears only the violation/write window rather than deleting the replacement offer. Before each write, a latch is created; authoritativetoolSuccess: falseand recognized failure prose clear it because no mutation occurred, while unknown outcomes retain it and require recovery. argument-less planning write the adapter stores the current target/revision in a protected write window. If the write deletes or corrupts state, the active marker, or another authority file, PostToolUse poisons the saved revision and later mutation calls remain blocked even when live authority can no longer be parsed. Adapter-ownednextrecovery clears that poison only after the engine returns a valid non-error directive. Raw audit appends have no authority. Fresh directives retire runtime state and rotate the source floor after generation. - session-start — reads the modern
session_idand persists it under the gitignored runtime session directory; the legacy channel derives a stable per-host-instance ID fromVSCODE_IPC_HOOK/VSCODE_PID. - terminal commands — newer builds that expose the submitted
/aidlc ...prompt run deterministic utilities at UserPromptSubmit. IDE 1.0.242 exposes an empty prompt, so the fallback recognizes the exactexecute_pwshaidlc-orchestrate.ts nextcall at PreToolUse, runs the classified utility once, and refuses the duplicate shell call. Both routes decode UTF-8 explicitly and remove terminal protocol/control bytes only from the plain-text relay; structured hook JSON and unrelated refusal paths are not rewritten. Modern turn/latch state is keyed by a hash ofsession_id, so concurrent chats cannot reuse one another's output; payloads without a session identity use one explicit legacy bucket. The 0.12 camelCase fallback reads the command fromtoolArgs.command. - 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. - record-human-turn — reads the modern
session_idand answer payload, or the legacyUSER_PROMPT; it can submit an exact directive-issued choice but never reveals, rotates, or transfers another chat's protected capability. - session-end / block — need no payload and never read stdin. Session-end reuses the identity persisted by SessionStart, with the legacy lifecycle fallback retained only where no approval authority is involved.
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).