> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rubixkube.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks: your own checks in Kepler's work

> Run your own script, HTTP call, MCP tool or model check at moments in Kepler's work: before a tool runs, when a message arrives, when a run ends, when a watcher fires. Hooks can block or ask. They never approve.

A hook runs your own check at a moment in Kepler's work: before a tool runs, when your message arrives, when a run finishes, when a watcher fires. Use one to ask before any `kubectl` on a prod context, to log every command to your audit system, or to add a line of context to every new chat.

A hook can block an action, ask you first, rewrite a tool's input, or add context. It can never approve something on your behalf. A hook that answers "allow" counts as no opinion, and Kepler's normal rules decide. Hooks only make Kepler stricter.

## Let Kepler write one

The quickest way to make a hook is to describe it:

```
/hook ask me before any kubectl command on a prod context
```

You can also just ask in chat. Kepler checks the hooks you already have, drafts the new one, and shows it on a card. The card says when the hook runs, what it runs, and what Kepler makes of the command: reads only, can change things, or reads secrets. Nothing is saved until you confirm the card. That holds in every posture, Yolo included.

* The hook goes into this workspace's `.kepler/hooks.json` by default. A hook for all workspaces needs you to ask for that, and the card says so.
* A hook command on the [always-blocked list](/kepler/permissions#always-blocked) is refused.
* Kepler can add hooks only in Assist and Yolo. In Observe it can explain and list your hooks, not add them.

## Add one in Settings

Open **Settings > Customize** and pick the **Hooks** chip. **New hook** builds a hook from fields: when it runs, which tools it applies to, the hook type, a timeout, **Block if it fails**, and **Save for** (all workspaces, or one). Each hook has its own switch, and **Hooks on** at the top switches every hook at once. See [Customize](/kepler/customize).

## Where hooks live

| File | Who writes it | When it runs |
| - | - | - |
| `~/.kepler/hooks.json` | You | As soon as it is saved. |
| `<workspace>/.kepler/hooks.json` | Anyone with the repository | Only after you trust it in **Settings > Customize > Hooks**. |

A workspace hook is trusted by a fingerprint of its exact content. If a `git pull` changes it, or adds a new one, it stops running until you review it again. Customize shows a banner when a workspace has hooks waiting for review. The fingerprint covers the hook entry, not a script it calls, so if a hook runs `./guard.sh`, review that script too.

Kepler's own file and shell tools can read these files but never write them. Only you change your hooks.

### Hooks you wrote for other agents

Kepler reads the same hook format Claude Code uses, so hooks you already wrote work here unchanged. Kepler finds them in `~/.claude/settings.json` and a project's `.claude/settings*.json`, but never runs them from there. Customize lists them under **From Claude Code** with an **Import** button, which copies the hook into the matching Kepler file.

## A first hook by hand

This hook asks you before any `kubectl` command that mentions `prod`. Put it in `~/.kepler/hooks.json`:

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "python3 ~/bin/prod-guard.py" }
        ]
      }
    ]
  }
}
```

And the script it runs, `~/bin/prod-guard.py`:

```python theme={null}
import json, sys

event = json.load(sys.stdin)
command = event["tool_input"].get("command", "")
if "kubectl" in command and "prod" in command:
    print(json.dumps({"hookSpecificOutput": {
        "permissionDecision": "ask",
        "permissionDecisionReason": "prod context",
    }}))
```

* `PreToolUse` is the event: before a tool runs.
* `"matcher": "Bash"` limits it to shell commands.
* The script gets the event as JSON on stdin and prints its answer as JSON.
* `ask` puts an approval card in front of you that reads "A hook asked: prod context".

## Events

Events that can block may stop or change the action. The rest are for context, logging and alerts.

| Event | Fires when | Can block |
| - | - | - |
| `SessionStart` | A chat's first run. | No. Its output becomes context. |
| `Setup` | First-run setup completes. | No |
| `UserPromptSubmit` | Your message arrives, from the app or a channel. | Yes |
| `UserPromptExpansion` | A custom slash command expands. | Yes |
| `PreToolUse` | Before any tool, built-in or MCP. | Yes: deny, ask or rewrite. |
| `PermissionRequest` | An approval card is about to show. | Deny only |
| `PermissionDenied` | You decline a card, or a rule or the always-blocked list stops a call. | No |
| `PostToolUse`, `PostToolUseFailure` | After a tool returns or fails. | No. Can add context. |
| `PostToolBatch` | Every tool call in one model step is done. | No |
| `Notification` | Kepler sends you a notification. | No |
| `MessageDisplay` | An answer is shown. | No |
| `Stop` | A run finishes its answer. | Yes: keep going, at most 3 times. |
| `StopFailure` | A run ends on an error. | No |
| `SubagentStart`, `SubagentStop` | A background lane starts or finishes. | `SubagentStop`: keep going once. |
| `TaskCreated`, `TaskCompleted` | A plan step is added or done. | No |
| `TeammateIdle` | A war room member ends its turn. | No |
| `InstructionsLoaded` | `kepler.md` or a skill is loaded. | No |
| `ConfigChange` | Settings, rules or hook files change. | No |
| `CwdChanged` | A chat's workspace folder changes. | No |
| `FileChanged` | A file watcher fires. | No |
| `PreModelSwitch`, `PostModelSwitch` | A chat's model changes. | `PreModelSwitch`: yes. |
| `Elicitation`, `ElicitationResult` | Kepler asks you a question in a card. | `Elicitation`: yes |
| `PreCompact`, `PostCompact` | The context is compacted. | No |
| `SessionEnd` | A chat is deleted. | No |

Kepler adds four events of its own, in the same format:

| Event | Fires when | Can block | The matcher matches |
| - | - | - | - |
| `WatcherFire` | A [watcher](/kepler/watchers) fires. | Yes: the wake is skipped. | The watcher's name |
| `AutomationStep` | Before and after each [automation](/kepler/automations) step. | Before: yes, and the step fails. | The step kind |
| `ChannelMessage` | A message arrives on a [channel](/kepler/channels) from someone you allowed. | Yes: the message is dropped and the sender told. | The channel name |
| `ApprovalResolved` | You approved or declined a card. | No | The tool name |

## What a hook receives

A hook gets JSON on stdin. It includes `session_id` (the chat), `transcript_path`, `cwd`, `hook_event_name` and `permission_mode` (the posture). Kepler adds `kepler_agent`. Tool events add `tool_name`, `tool_input`, `tool_use_id` and `kepler_tool_name`, and `tool_response` afterwards.

Tool names are mapped so matchers written for Claude Code work. A matcher may use either name.

| Name in the matcher | Kepler tool | Input |
| - | - | - |
| `Bash` | `run_command`, `run_read`, `run_read_on` | `{command}` |
| `Write` | `write_file` | `{file_path, content}` |
| `Edit` | `edit_file` | `{file_path, old_string, new_string}` |
| `WebFetch` | `web_fetch` | `{url}` |
| `AskUserQuestion` | `ask_user` | unchanged |
| `mcp__<server>__<tool>` | That MCP tool | unchanged |

A matcher made of letters, digits, `_`, `|` and `,` means exact names, such as `Bash|Edit`. Anything else is a regular expression searched anywhere in the name, so `Edit.*` also matches `NotebookEdit`. Anchor it (`^Edit$`) when that matters.

Secrets are masked in everything a hook receives and everything it prints.

## What a hook can answer

| Exit code | Meaning |
| - | - |
| `0` | The JSON on stdout is the answer. For `SessionStart` and `UserPromptSubmit`, plain text becomes context. |
| `2` | Block, with stderr as the reason. |
| Anything else | An error. The action continues and the error is shown. |

The JSON can carry `decision: "block"` with a `reason`; `hookSpecificOutput.permissionDecision` (`deny` or `ask`) with `permissionDecisionReason`, `updatedInput` or `additionalContext`; `continue: false` with a `stopReason`; and `systemMessage`.

Where Kepler differs from Claude Code:

* `allow` and `defer` are ignored. Hooks can only make Kepler stricter.
* A hook's ask is not cleared by your allow rules or by a session allow. Posture, block rules and the always-blocked list still win over everything.
* A rewritten input (`updatedInput`) is checked again by Kepler's rules.
* Add `"failClosed": true` to a hook to turn a crash, a timeout or unreadable JSON into a deny. Use it for security hooks.
* On runs nobody is watching (watchers, automations, channels), a hook's ask counts as a deny.
* Context over 10,000 characters is saved to a file and Kepler gets its path. Nothing is cut.
* Tools that run on the model provider's side, such as native web search, skip hooks.
* When several hooks match, they run in parallel and the strictest answer wins.

Timeouts are 60 seconds by default, 30 for `UserPromptSubmit`, and 600 at most.

## Hook types

| Type | What it does |
| - | - |
| `command` | Runs a shell command in your login shell, with `KEPLER_PROJECT_DIR` set. A command on the always-blocked list never runs, in a hook either. |
| `http` | POSTs the event to `url`. The response body is the answer. Header values can read only the environment variables you list in `allowedEnvVars`. |
| `mcp_tool` | Calls `tool` on a connected MCP `server` with the event as input. Tools that look like writes are refused. |
| `prompt` | Your worker model reads the event and your `prompt` and answers ok or not. Not ok blocks. |
| `agent` | Like `prompt`, but a short read-only helper can look things up first. It never writes and never asks you anything. |

An `http` hook looks like this:

```json theme={null}
{ "type": "http", "url": "https://audit.example.com/hook",
  "headers": { "Authorization": "Bearer $AUDIT_TOKEN" },
  "allowedEnvVars": ["AUDIT_TOKEN"] }
```

## When a hook misbehaves

* Every hook run is logged. See them under **Settings > Plan & Usage**, on the **Hooks** tab of Activity, or in `~/.kepler/hooks/log-YYYY-MM-DD.jsonl`.
* A mistake in `hooks.json` shows as a banner in Customize. The broken entry is skipped and every other hook keeps running.
* If your login shell prints text before your JSON, Kepler reads the last line that is a JSON object.
* A hook in a workspace that stopped running usually changed and needs review again.

## Where to go next

<CardGroup cols={2}>
  <Card title="Customize" icon="sliders" href="/kepler/customize">
    Where hooks, skills, commands, agents and plugins are managed.
  </Card>

  <Card title="Permissions" icon="lock" href="/kepler/permissions">
    The rules hooks sit on top of.
  </Card>

  <Card title="Plugins" icon="puzzle-piece" href="/kepler/plugins">
    Plugins can bring hooks too, held to the same rules.
  </Card>

  <Card title="Postures" icon="shield-halved" href="/kepler/postures">
    Observe, Assist and Yolo.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.