Hooks
Hooks are shell commands that fire before or after any tool call, filtered by tool-name patterns. They’re the main way to customize AI Butler’s behavior without writing Go code.
Use Cases
Section titled “Use Cases”- Auto-format on save — run
gofumptorprettierafter everyfile.edit - Block dangerous commands — refuse
shell.execwhen the command matches a dangerous pattern - Audit tool use — append every
shell.execto a log file - Notify on completion — ping a webhook when a long task finishes
- Enforce policy — run a linter and block commits that fail it
Configuration
Section titled “Configuration”Hooks live in config.yaml under configurations.hooks. There are two events: pre_tool_use (before the tool runs) and post_tool_use (after it completes) — plus a verify shorthand (beta) described below.
Each hook has a shell command and a list of tool-name tools patterns. If the tool being called matches any pattern, the hook fires.
configurations: hooks: pre_tool_use: - command: "echo 'blocked'; exit 2" tools: ["shell.exec"] # fires on every shell call
post_tool_use: - command: "gofumpt -w ." tools: ["file.edit", "file.write"]
- command: "printf '%s %s\n' \"$(date)\" \"$HOOK_TOOL_NAME\" >> /var/log/aibutler-audit.log" tools: ["*"] # fires on every tool callTool Name Patterns
Section titled “Tool Name Patterns”A pattern is an exact tool name, a prefix ending in *, or * alone. An empty tools list matches every tool.
| Pattern | Matches |
|---|---|
file.edit | Exact match only |
file.* | All file tools (file.read, file.write, file.edit, file.list, file.search) |
git.* | All git tools |
iot.safety.* | Safety-tier IoT tools only |
* | Every tool call |
What Hooks Receive
Section titled “What Hooks Receive”Hook commands get the tool-call context two ways.
Environment variables:
| Variable | Value |
|---|---|
HOOK_EVENT | PreToolUse or PostToolUse |
HOOK_TOOL_NAME | Name of the tool being called (e.g. file.edit) |
HOOK_TOOL_INPUT | JSON-encoded tool input |
HOOK_TOOL_OUTPUT | Tool output (post-hooks only) |
HOOK_TOOL_IS_ERROR | true or false (post-hooks only) |
JSON on stdin: the same data arrives as a single JSON object — hook_event_name, tool_name, tool_input (parsed), tool_input_json (raw string), tool_output, tool_result_is_error — for hooks that prefer jq over environment variables.
Hooks time out after 10 seconds.
Exit Code Semantics
Section titled “Exit Code Semantics”Pre-hooks can block tool execution by their exit code:
| Exit code | Meaning |
|---|---|
0 | Success — tool call proceeds; anything the hook printed to stdout is passed to the agent as feedback |
2 | Block — tool call is refused; the hook’s stdout is returned to the agent as the reason |
| anything else | Non-fatal failure — a warning is logged, the tool call still proceeds |
Post-hooks can’t undo the already-completed call, but their stdout is appended to the tool output inside a sanitized <hook_feedback untrusted="true"> block, so the model sees it and can react.
Post-Mutation Verification — verify (beta)
Section titled “Post-Mutation Verification — verify (beta)”configurations.hooks.verify is shorthand for a post-mutation verification hook. It’s off by default — setting a command opts in. After the listed tools mutate files (default: file.write, file.edit), the command runs and its real output is fed back into the model’s context, appended to the tool output in a sanitized block marked untrusted:
configurations: hooks: verify: command: "go build ./... 2>&1 | head -20" tools: ["file.write", "file.edit"] # the default when omittedThe point is grounding: after an edit, the model corrects against what the compiler, linter, or test runner actually said — not against its own assessment of the edit. A verify entry expands into an ordinary post_tool_use hook under the hood, so everything above about matching, stdout feedback, and timeouts applies.
Example: Block Destructive Shell Commands
Section titled “Example: Block Destructive Shell Commands”configurations: hooks: pre_tool_use: - command: | if echo "$HOOK_TOOL_INPUT" | grep -qE 'rm -rf /|dd if=|mkfs\.'; then echo "Refused: destructive shell command" exit 2 fi tools: ["shell.exec"]Example: Auto-format Go Files
Section titled “Example: Auto-format Go Files”configurations: hooks: post_tool_use: - command: | path=$(echo "$HOOK_TOOL_INPUT" | jq -r .path) if [[ "$path" == *.go ]]; then gofumpt -w "$path" fi tools: ["file.edit", "file.write"]Example: Audit Log
Section titled “Example: Audit Log”configurations: hooks: post_tool_use: - command: "printf '%s %s error=%s\n' \"$(date -Iseconds)\" \"$HOOK_TOOL_NAME\" \"$HOOK_TOOL_IS_ERROR\" >> /var/log/aibutler-audit.log" tools: ["*"]Security Notes
Section titled “Security Notes”Hooks run as the same OS user as AI Butler itself, via sh -c, outside the workspace sandbox — treat hook commands as trusted configuration, on par with editing config.yaml itself. Do not put secrets in hook commands; they’re logged verbatim on failure. Hook output that reaches the model is sanitized and wrapped in an explicitly untrusted block, but a hook command can still do anything your user account can — review what you install.