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 missing command are skipped too. timeout is in seconds and defaults to 30; a hook with async: true does not block the turn.
  • Execution: the engine starts a bash -c process 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.json itself. After you trust, disable, or change hooks in "Settings → Hooks", open sessions rebuild with the new manifest on their next turn; editing hooks.json directly 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 the hooks key in settings.json are only an import source; they are not executed themselves.