Core API
Hooks#
Declare lifecycle hooks once against normalized events; the framework synthesizes the right shape per host paradigm and formats your reply into the host's native control surface.
Hooks vary the most across hosts
This page is the API reference. For the full canonical-event × platform mapping matrix, per-platform tabs, and the Claude Code ↔ Kilo CLI comparison, see the dedicated Hooks: cross-platform guide.interface HooksConfig {
SessionStart?: HookDefinition<"SessionStart">;
SessionEnd?: HookDefinition<"SessionEnd">;
UserPromptSubmit?: HookDefinition<"UserPromptSubmit">;
PreToolUse?: HookDefinition<"PreToolUse">;
PostToolUse?: HookDefinition<"PostToolUse">;
PreCompact?: HookDefinition<"PreCompact">;
Stop?: HookDefinition<"Stop">;
Notification?: HookDefinition<"Notification">;
PermissionRequest?: HookDefinition<"PermissionRequest">;
PostToolUseFailure?: HookDefinition<"PostToolUseFailure">;
SubagentStart?: HookDefinition<"SubagentStart">;
SubagentStop?: HookDefinition<"SubagentStop">;
PostCompact?: HookDefinition<"PostCompact">;
}
interface HookDefinition<E> {
matcher?: string; // regex on tool name (tool events, incl. PermissionRequest /
// PostToolUseFailure) or on agent type (SubagentStart /
// SubagentStop); empty = match all
handler(event: EventPayloadMap[E]):
HookResponse | void | Promise<HookResponse | void>;
}matcher is a regex matched against the tool name (tool events, incl. PermissionRequest / PostToolUseFailure) or against the agent type (SubagentStart / SubagentStop); empty or omitted matches all. It is rendered into each host's native matcher syntax where supported, else evaluated by the universal entrypoint at runtime.
Normalized events#
Every event extends a base { hostPlatform, connectorId, sessionId, projectDir?, raw } (sessionId is "" when the host provides none; raw is the verbatim host payload for escape-hatch use):
| Event | Extra payload fields |
|---|---|
SessionStart | source: "startup" | "compact" | "resume" | "clear" |
SessionEnd | reason?: string |
UserPromptSubmit | prompt: string |
PreToolUse | toolName: string, toolInput: Record<string, unknown> |
PostToolUse | toolName, toolInput, toolOutput?: string, isError?: boolean |
PreCompact | trigger?: "auto" | "manual" |
Stop | stopHookActive?: boolean |
Notification | message: string |
PermissionRequest | toolName, toolInput, permissionSuggestions?: unknown[] (host dialog suggestions, passthrough) |
PostToolUseFailure | toolName, toolInput, toolUseId?, error: string, isInterrupt?: boolean, durationMs?: number |
SubagentStart | agentId?: string, agentType?: string |
SubagentStop | agentId?, agentType?, agentTranscriptPath?, lastAssistantMessage?, stopHookActive? |
PostCompact | trigger?: "auto" | "manual" |
HookResponse#
Return a subset of these fields; the adapter formats it into the host's native reply (exit codes / JSON / control fields) and drops fields the host can't honor, reporting the degradation.
| Field | Type | Default | Notes |
|---|---|---|---|
decision | "allow" | "deny" | "modify" | "context" | "ask" | — | Drives the host's native reply. Default allow (handler returns void). |
reason | string | — | Shown to the model/user; expected for deny / ask. |
updatedInput | Record<string, unknown> | — | Replacement tool input — only with "modify" (PreToolUse / PermissionRequest). |
additionalContext | string | — | Injected as soft guidance — with "context" or on SessionStart. |
updatedOutput | string | — | Rewritten tool output — PostToolUse only, where the host supports it. |
Decision semantics
| decision | Meaning |
|---|---|
allow | Pass through (default when the handler returns void). On PermissionRequest ONLY, an explicit allow is an ACTIVE grant that suppresses the host's permission dialog (a void return falls through to the native dialog). |
deny | Block the tool call / stop the action. On SubagentStop this keeps the subagent running with reason as its next instruction (Stop semantics); on the feedback-only events (PostToolUseFailure, SubagentStart) it degrades to context carrying the reason. |
modify | Replace tool input with updatedInput (PreToolUse / PermissionRequest). |
context | Inject additionalContext as soft guidance (on SubagentStart it lands in the SUBAGENT's conversation, before its first prompt). |
ask | Prompt the user to confirm. On PermissionRequest this falls through to the native dialog (the dialog IS the ask). |
hooks: {
PreToolUse: {
matcher: "acme_write",
async handler(evt) {
// evt: { hostPlatform, connectorId, sessionId, projectDir?, raw,
// toolName, toolInput }
if (evt.toolName === "acme_write")
return { decision: "ask", reason: "Confirm Acme DB write" };
return { decision: "allow" };
},
},
}Three paradigms#
The framework picks the right synthesis from the host's detected paradigm:
| Paradigm | Hosts | How hooks are delivered |
|---|---|---|
json-stdio | 24 | Host pipes JSON to a command on stdin and reads JSON / exit-code back. One universal entrypoint (agent-connector hook <platform> <event> --connector <id>) reads the payload, normalizes it, runs your handler, and formats the reply. |
ts-plugin | 8 | Host loads a framework-generated JS/TS module exporting lifecycle functions that import your handler — the native shape these hosts expect. |
mcp-only | 10 | No hook layer; only the MCP server is installed and hooks are reported unavailable for that host. |
Fail-open runtime contract
The hook entrypoint never rejects, so a framework or handler bug can't wedge a host's tool call.Native hooks passthrough#
The normalized union stays small on purpose — hosts ship far more events than 13 (Claude Code alone has 30). For host-only events, platforms.<id>.nativeHooks wires any native event by its verbatim name — including events a host adds in the future — with zero agent-connector releases:
platforms: {
"claude-code": {
nativeHooks: {
// key = the HOST's event name, VERBATIM — any of Claude's 30 events
TaskCompleted: {
async handler(evt) {
// evt: { event, hostPlatform, sessionId, projectDir?, raw }
// evt.raw = Claude's stdin JSON, untouched (snake_case and all)
// return value = the VERBATIM stdout JSON reply (exit 0);
// void = exit 0 with no output
return { continue: false, stopReason: "All tasks done — wrap up." };
},
},
},
},
}Raw in, verbatim out — and exit 0 only
No normalization and noHookResponse mapping: the handler reads the host's raw stdin payload (evt.raw) and its return value is the verbatim stdout JSON reply. void → exit 0 with no output; any throw fails open. Exit-2 blocking semantics are not modeled in v1 — JSON-on-exit-0 decision control covers Claude Code's events. Declaring one of the 13 normalized event names here is a ConnectorConfigError (use hooks for those). 16 adapters set supportsNativeHooks today (claude-code, codebuddy, opencode, cursor, gemini-cli, qwen-code, amp, kimi, omp, hermes, jetbrains-copilot, copilot-cli, continue, nemoclaw, openclaw, grok-cli); adapters that leave it unset skip-warn, never silently. An event is promoted into the normalized union once ≥3 hosts ship a native analog — TaskCreated / TaskCompleted are the first candidates.