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.
HooksConfig
ts
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):

EventExtra payload fields
SessionStartsource: "startup" | "compact" | "resume" | "clear"
SessionEndreason?: string
UserPromptSubmitprompt: string
PreToolUsetoolName: string, toolInput: Record<string, unknown>
PostToolUsetoolName, toolInput, toolOutput?: string, isError?: boolean
PreCompacttrigger?: "auto" | "manual"
StopstopHookActive?: boolean
Notificationmessage: string
PermissionRequesttoolName, toolInput, permissionSuggestions?: unknown[] (host dialog suggestions, passthrough)
PostToolUseFailuretoolName, toolInput, toolUseId?, error: string, isInterrupt?: boolean, durationMs?: number
SubagentStartagentId?: string, agentType?: string
SubagentStopagentId?, agentType?, agentTranscriptPath?, lastAssistantMessage?, stopHookActive?
PostCompacttrigger?: "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.

FieldTypeDefaultNotes
decision"allow" | "deny" | "modify" | "context" | "ask"—Drives the host's native reply. Default allow (handler returns void).
reasonstring—Shown to the model/user; expected for deny / ask.
updatedInputRecord<string, unknown>—Replacement tool input — only with "modify" (PreToolUse / PermissionRequest).
additionalContextstring—Injected as soft guidance — with "context" or on SessionStart.
updatedOutputstring—Rewritten tool output — PostToolUse only, where the host supports it.

Decision semantics

decisionMeaning
allowPass 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).
denyBlock 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.
modifyReplace tool input with updatedInput (PreToolUse / PermissionRequest).
contextInject additionalContext as soft guidance (on SubagentStart it lands in the SUBAGENT's conversation, before its first prompt).
askPrompt the user to confirm. On PermissionRequest this falls through to the native dialog (the dialog IS the ask).
agent-connector.config.mjs
ts
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:

ParadigmHostsHow 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:

agent-connector.config.mjs
ts
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 no HookResponse 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.