Skip to content

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.

  • Auto-format on save — run gofumpt or prettier after every file.edit
  • Block dangerous commands — refuse shell.exec when the command matches a dangerous pattern
  • Audit tool use — append every shell.exec to 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

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 call

A pattern is an exact tool name, a prefix ending in *, or * alone. An empty tools list matches every tool.

PatternMatches
file.editExact 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

Hook commands get the tool-call context two ways.

Environment variables:

VariableValue
HOOK_EVENTPreToolUse or PostToolUse
HOOK_TOOL_NAMEName of the tool being called (e.g. file.edit)
HOOK_TOOL_INPUTJSON-encoded tool input
HOOK_TOOL_OUTPUTTool output (post-hooks only)
HOOK_TOOL_IS_ERRORtrue 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.

Pre-hooks can block tool execution by their exit code:

Exit codeMeaning
0Success — tool call proceeds; anything the hook printed to stdout is passed to the agent as feedback
2Block — tool call is refused; the hook’s stdout is returned to the agent as the reason
anything elseNon-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 omitted

The 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.

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"]
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"]
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: ["*"]

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.