| # Hooks |
|
|
| Hooks are an automatic trigger mechanism: you tell Kimi Code CLI in advance "whenever X happens, run this script." The script runs on your local machine, and you can put any logic inside it. Typical use cases: |
|
|
| - **Security interception**: Before the Agent executes a shell command, check whether it contains dangerous operations (such as `rm -rf`) and block execution if so |
| - **Desktop notifications**: When a background task completes, pop up a system notification to bring you back to review the results |
| - **Automatic checks**: Each time the user submits a message, automatically append some background information to the context (such as the current Git branch) |
|
|
| ## How Hooks Work |
|
|
| Configuring a hook rule requires specifying three things: **which event to trigger on**, **which targets to match**, and **which script to run**. |
|
|
| When triggered, the CLI packages the event's details (trigger reason, tool name, command content, etc.) into JSON and passes it to your script via **standard input** (stdin). The script reads this information and decides how to respond. |
|
|
| The script's response is determined by two things: |
|
|
| - **Exit code**: `0` means allow, `2` means block, other non-zero values default to allow |
| - **Standard output** (stdout): can include explanatory text |
|
|
| Even if the script errors or times out, the CLI **will not interrupt your work** as a result. This "allow on failure" design is called fail-open, preventing hook errors from becoming blockers. |
|
|
| ::: warning Note |
| Precisely because of fail-open, Hooks are suitable for alerts and lightweight interception, but **should not be used as the sole security barrier**. For truly high-risk operations, rely on permission approvals and manual confirmation. |
| ::: |
|
|
| ## Quick Start: A Minimal Hook |
|
|
| The following hook flashes a notification in the terminal title bar each time a background task completes (macOS requires `terminal-notifier` to be installed): |
|
|
| ```toml |
| # Written in ~/.kimi-code/config.toml |
| [[hooks]] |
| event = "Notification" # Trigger: when a background task status changes |
| matcher = "task\\.completed" # Only care about "completed" notifications |
| command = "terminal-notifier -title Kimi -message 'Task done'" |
| ``` |
|
|
| Save the config, start a new session, and a notification will appear the next time a background task completes. |
|
|
| ## Configuration |
|
|
| All hook rules are written in the `[[hooks]]` array in `~/.kimi-code/config.toml`, where each entry is one rule: |
|
|
| | Field | Type | Required | Description | |
| | --- | --- | --- | --- | |
| | `event` | `string` | Yes | Trigger event name; must be one of the events in the [event reference](#event-reference) | |
| | `matcher` | `string` | No | A regular expression to filter event targets; if omitted, matches all | |
| | `command` | `string` | Yes | The shell command to run when triggered | |
| | `timeout` | `integer` | No | Timeout in seconds, range 1β600; defaults to 30 seconds | |
|
|
| `[[hooks]]` only allows these four fields; extra fields will cause the config file to fail to load. |
|
|
| **When multiple rules match the same event**, all matching hooks run in parallel; multiple rules with identical `command` values run only once. |
|
|
| The working directory for hook commands is the current session's project directory. |
|
|
| <details> |
| <summary>Process group and timeout handling</summary> |
|
|
| On non-Windows platforms, hook processes run in a separate process group; on timeout, the CLI first sends a signal to give the script a chance to clean up, then forcibly terminates it. |
|
|
| </details> |
|
|
| ### Event Data Format |
|
|
| Each time a hook triggers, the CLI passes the following base information to the script via stdin: |
|
|
| ```json |
| { |
| "hook_event_name": "PreToolUse", |
| "session_id": "session_abc", |
| "session_title": "Fix the login page", |
| "client_type": "kimi_code_cli", |
| "cwd": "/path/to/project" |
| } |
| ``` |
|
|
| Specific events will also include additional fields (such as tool name and command content); see the [event reference](#event-reference). All field names use snake_case. |
| |
| ## Return Values |
| |
| After the script exits, the CLI determines the hook's intent based on the exit code: |
| |
| | Exit code | Meaning | CLI behavior | |
| | --- | --- | --- | |
| | `0` | Normal exit, allow | Continue execution; stdout content (if any) may be appended to context | |
| | `2` | Intentional block | Stop the current operation; stderr content (printed via `console.error`) is used as the reason for blocking | |
| | Other non-zero | Script error | Default allow (fail-open) | |
| | Timeout or crash | Script exception | Default allow (fail-open) | |
| |
| You can also return a JSON object via stdout to block: |
| |
| ```json |
| { |
| "hookSpecificOutput": { |
| "permissionDecision": "deny", |
| "permissionDecisionReason": "Please use rg instead of grep" |
| } |
| } |
| ``` |
| |
| ::: info Which events support blocking? |
| Only **blockable events** (`PreToolUse`, `Stop`, `UserPromptSubmit`) have return values that affect the main flow. All other events are **observation-only events**: they fire and forget, and the main flow is unaffected regardless of what the script returns. |
| ::: |
| |
| ## Event Reference |
| |
| | Event | Matcher matches | Supports blocking? | Description | |
| | --- | --- | --- | --- | |
| | `UserPromptSubmit` | The text submitted by the user | β | Triggered when the user sends a message; returned text is appended to context; blocking skips the model call this turn | |
| | `UserPromptQueued` | The queued prompt text | β | Triggered when a message is queued while a turn is still running; payload includes `prompt_id`, `prompt`, `queue_length` | |
| | `PreToolUse` | Tool name | β | Triggered before a tool call (before permission checks); the tool will not execute if blocked | |
| | `Stop` | Empty string | β | Triggered when the model is about to end the turn; if blocked, a message can be appended to let the model continue | |
| | `TurnStarted` | Turn origin kind (e.g. `user`, `task`, `system_trigger`) | β | Triggered when a new turn begins; payload includes `turn_id`, `origin_kind`, `origin_name`, `prompt` | |
| | `PostToolUse` | Tool name | β | Triggered after a tool executes successfully | |
| | `PostToolUseFailure` | Tool name | β | Triggered after a tool fails or is blocked | |
| | `PermissionRequest` | Tool name | β | Triggered just before waiting for user approval | |
| | `PermissionResult` | Tool name | β | Triggered after approval completes | |
| | `SessionStart` | `startup` or `resume` | β | Triggered after a session starts or resumes; payload includes `source`, `model`, `profile` | |
| | `SessionEnd` | `exit` or `archive` | β | Triggered after a session closes; `archive` means the session was archived rather than exited | |
| | `SessionHeartbeat` | Empty string | β | Triggered every 60 seconds while the session is alive; the timer runs only when this event is configured; payload includes `uptime_ms` | |
| | `SubagentStart` | Sub-agent name | β | Triggered before a sub-agent starts running | |
| | `SubagentStop` | Sub-agent name | β | Triggered after a sub-agent completes successfully | |
| | `TaskStarted` | Task kind (`agent`, `process`, or `question`) | β | Triggered when a background task starts; payload includes `task_id`, `description`, `detached` | |
| | `StopFailure` | Error type | β | Triggered after the current turn fails due to an error | |
| | `Interrupt` | Empty string | β | Triggered when the user interrupts the turn (e.g. pressing Esc); not fired for timeouts or programmatic aborts; fires in place of `Stop`; payload includes `reason` | |
| | `PreCompact` | `manual` or `auto` | β | Triggered before context compaction begins; return values are completely ignored | |
| | `PostCompact` | `manual` or `auto` | β | Triggered after context compaction completes | |
| | `Notification` | Notification type (e.g. `task.completed`) | β | Triggered when a background task status changes | |
|
|
| ## Example: Blocking Dangerous Shell Commands |
|
|
| The following hook checks the command content before the Agent calls the `Bash` tool and blocks it if `rm -rf` is detected: |
|
|
| ```toml |
| [[hooks]] |
| event = "PreToolUse" |
| matcher = "Bash" |
| command = "node ~/.kimi-code/hooks/block-dangerous-bash.mjs" |
| timeout = 5 |
| ``` |
|
|
| ```js |
| // block-dangerous-bash.mjs |
| // Read event data passed by the CLI from stdin |
| let input = ''; |
| process.stdin.on('data', (chunk) => { input += chunk; }); |
| process.stdin.on('end', () => { |
| const payload = JSON.parse(input); // Parse event data |
| const command = payload.tool_input?.command ?? ''; |
| |
| if (command.includes('rm -rf')) { |
| // Explain the blocking reason via stderr; exit code 2 means block |
| console.error('Dangerous command detected, blocked'); |
| process.exit(2); |
| } |
| // Normal exit (exit code 0) means allow |
| }); |
| ``` |
|
|
| After blocking, Kimi Code CLI writes the blocking reason back into the context, and the model can use this to choose a safer alternative. |
|
|
| ::: warning Note |
| This example only demonstrates the blocking mechanism and is not a production-grade security parser. Real scenarios are better served by whitelists, or a dedicated shell parser to handle quoting, variable expansion, and multi-command sequences. |
| ::: |
|
|
| ## Next steps |
|
|
| - [Configuration](#configuration) β Full field reference for `[[hooks]]` in `config.toml` |
| - [Agents and sub-agents](./agents.md) β Use the `SubagentStop` event to trigger notifications after a sub-agent completes |
|
|