File size: 9,244 Bytes
f0634fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | # 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
|