Hooks
Run shell commands on 17 lifecycle events; hooks can also allow an action, block it, or turn it into a prompt.
Hooks are shell commands that run at fixed lifecycle points, such as before a tool call or after a turn ends. You can use them to wire formatting, tests, notifications, and similar actions into Klaus's loop.
- There are 17 events you can attach to: session start/end, when you submit a prompt, before/after a tool call, tool failure, turn stop, sub-agent start/end, before/after compaction, notification, Setup, task create/complete, and worktree create/remove. Entries with a misspelled event name are silently dropped without an error, so they look as if the hook was never written.
- Locations: global
~/.klaus/hooks.json, project<project>/.klaus/hooks.json. The file shape is "event → several groups → each group has one optional matcher plus a list of hooks"; the matcher belongs to the group, not to individual hooks. For the same event, global hooks run before project hooks, and within a file hooks run in the order they are written. - Only entries with
type: "command"actually execute; other types in the same group are skipped without affecting neighboring command entries, and entries missingcommandare skipped too.timeoutis in seconds and defaults to 30; a hook withasync: truedoes not block the turn. - Execution: the engine starts a
bash -cprocess and feeds it a JSON document on stdin; if stdout is JSON, it is parsed as structured output. A nonzero exit code or non-JSON output marks the hook run as failed but does not block the tool call. - Hooks are more than bystanders: a pre-tool-call hook can return allow, block, or "ask instead" directly in its output — when it blocks, the call fails outright and not even the permission card appears; a prompt hook can drop your entire message; a stop hook can keep the turn from ending. When multiple hooks take a position at once, blocking wins (the first block decides), then asking, and only then allowing.
- The hook manifest is handed to the engine with session initialization; the engine does not read
hooks.jsonitself. After you trust, disable, or change hooks in "Settings → Hooks", open sessions rebuild with the new manifest on their next turn; editinghooks.jsondirectly in an editor does not trigger a rebuild, so it takes effect the next time hooks are changed in the app or in a new session.
There is also a trust gate
Writing a hook into hooks.json does not mean it will run. Every hook declaration must be explicitly trusted by you in Settings → Hooks before it is injected into a session, and trust is recorded as "scope + file path + event + position within the group + hash of the declaration content". So:
- A new declaration that has never been trusted has the status "new hook" and stays unexecuted.
- A declaration you trusted before whose content was later changed gets the status "content changed, re-trust required"; the old trust is voided and it stays unexecuted — this guards against a skill's updater quietly rewriting its bundled hooks and then continuing to run code on the strength of your earlier approval.
- A declaration you manually disabled has the status "disabled"; one that is trusted and unchanged is "trusted".
"Settings → Hooks" groups hooks by origin into three groups: the global config file, plugins, and projects. Within each group hooks are collected by event; expand an event to see each hook's actual command and its status. Plugin hooks show only the event and count. You can trust one at a time or use "trust all pending". This is also the most common reason "the hook is written but doesn't run": either the event name was misspelled and dropped, or it hasn't been trusted yet. Also, hook execution failures no longer inject content into the conversation record — they are written to the host log only.
- Hooks bundled with plugins do not go through this path: the engine loads them directly from the plugin ledger (if the client reported them as well, every plugin hook would fire twice per event).
- Hooks are read only from
hooks.json. When importing from another client, hooks written under thehookskey insettings.jsonare only an import source; they are not executed themselves.