Hooks (src/hooks/)
This page is generated from
docs/lld/hooks.mdin the repository.
Purpose
Section titled “Purpose”Run the user’s own commands before and after tool calls: guards that block calls, and checks that give the model feedback (for example lint errors). Hooks can block; they can never approve.
Config (config.ts)
Section titled “Config (config.ts)”~/.garuda/hooks.json (user, trusted) and <root>/.garuda/hooks.json (project, needs consent):
{ "hooks": { "preToolUse": [{ "tools": ["bash(git push*)"], "command": "echo 'Pushes need a review.' >&2; exit 2" }], "postToolUse": [{ "tools": ["edit_file", "write_file"], "command": "npx eslint \"$GARUDA_FILE\" >&2 || exit 2", "timeoutMs": 30000, "network": false }]} }- Strict schema; events:
preToolUse,postToolUse. tools: permission rule syntax; empty = every tool. Invalid rules are reported and the hook is skipped.hooksHash(project hooks): sha256 of the canonical list; consent is pinned to it intrust.json(hooks: { <root>: <hash> }).
Consent (runtime, before the first turn)
Section titled “Consent (runtime, before the first turn)”User hooks are active at once. Project hooks: if the stored hash differs, Garuda asks “Run this project’s hooks?” and shows every event, tool list, network flag and command in full, and whether an OS sandbox exists. Answers: once, remember (store the hash), deny (ignore the project’s hooks). A changed file asks again (“changed since you allowed them”).
The consent and /hooks show each command and tool pattern as one clean line (cleanLine, 0.14.1,
review): an escape code, a carriage return or a newline in a project’s hook cannot hide its real start
or add a fake line.
Runner (runner.ts)
Section titled “Runner (runner.ts)”HookRunner implements ToolHooks; the registry calls it around each tool call.
| Event | When | Exit 0 | Exit 2 | Other exit, timeout, crash |
|---|---|---|---|---|
preToolUse |
After describe(), before the permission check |
continue | block; stderr is the reason for the model | block (fail closed) + warning for the user |
postToolUse |
After the call | nothing | append <hook_feedback>stderr</hook_feedback> to the result |
warning only |
Matching: a hook applies when one of its rules matches the call (deny-mode matching, so for commands one
matching part is enough: git status && git push matches bash(git push*)).
Execution:
- The team policy first (0.14, review):
permissions.commandPolicyDenial(command, false)appliesrequireSandbox(with no OS sandbox every hook is refused) anddisallowedCommands. A refused hook does not run; it counts as a failed hook (a preToolUse hook then blocks the call) and the audit log getsdeny_policy. executor.run(command, policy, { signal, env })withpolicy = execPolicy(timeoutMs, { sandbox: true }),networkfrom the hook, output capped at 8 000 bytes. Each run goes to the audit log (hookevent: command, duration, exit code; risk high with network).- Data: a 0600 temp file (
$GARUDA_HOOK_INPUT) with{ event, tool, input, target, result?, root }, deleted afterwards; variables$GARUDA_HOOK_EVENT,$GARUDA_TOOL,$GARUDA_FILE(absolute, for path targets),$GARUDA_COMMAND,$GARUDA_URL. - Hook output is cleaned, capped at 4 000 characters, and tags are neutralized.
Other parts
Section titled “Other parts”/hooks lists the active hooks. The system prompt explains “Blocked by a hook” and <hook_feedback> when
hooks exist. The evals run with hooks: false.
test/hooks.test.ts: config and hashing, block with exit 2 before the approval, fail closed on exit 1 and
on a timeout, event data in the file and variables, post-hook feedback and warnings, and project consent
(deny, remember, pinned, changed).