Hooks#
Hooks are shell actions that run before or after specific events, useful for notifications, injecting context, modifying inputs, or blocking tool calls.
Hook Types#
| Type | When | Can Modify |
|---|---|---|
sessionStart |
Server initialized | - |
sessionEnd |
Server shutting down | - |
chatStart |
New chat or resumed chat | Can inject additionalContext |
chatEnd |
Chat deleted | - |
preRequest |
Before prompt sent to LLM | Can rewrite prompt, inject context, stop request |
postRequest |
After prompt finished | - |
preToolCall |
Before tool execution | Can modify args, override approval, reject |
postToolCall |
After tool execution | Can inject context for next LLM turn |
Hook Options#
matcher: Regex forserver__tool-name, only for*ToolCallhooks.visible: Show hook execution in chat (default:true).runOnError: ForpostToolCall, run even if tool errored (default:false).
Execution Details#
- Order: Alphabetical by key. Prompt rewrites chain; argument updates merge (last wins).
- Conflict: Any rejection (
denyor exit2) blocks the call immediately. - Timeout: Actions time out after 30s unless
"timeout": msis set.
Input / Output#
Hooks receive JSON via stdin with event data (top-level keys snake_case, nested data preserves case). Common fields:
- All hooks:
hook_name,hook_type,workspaces,db_cache_path - Chat hooks add:
chat_id,agent,behavior(deprecated alias) - Tool hooks add:
tool_name,server,tool_input,approval(pre) ortool_response,error(post) chatStartadds:resumed(boolean)
Hooks can output JSON to control execution:
{
"additionalContext": "Extra context for LLM", // injected as XML block
"replacedPrompt": "New prompt text", // preRequest only
"updatedInput": {"key": "value"}, // preToolCall: merge into tool args
"approval": "allow" | "ask" | "deny", // preToolCall: override approval
"continue": false, // stop processing (with optional stopReason)
"stopReason": "Why stopped",
"suppressOutput": true // hide hook output from chat
}
Plain text output (non-JSON) is treated as additionalContext.
To reject a tool call, either output {"approval": "deny"} or exit with code 2.
Examples#
~/.config/eca/config.json
{
"hooks": {
"notify-me": {
"type": "postRequest",
"visible": false,
"actions": [{"type": "shell", "shell": "notify-send 'Prompt finished!'"}]
}
}
}
~/.config/eca/hooks/my-hook.sh
jq -e '.approval == "ask"' > /dev/null && canberra-gtk-play -i complete
~/.config/eca/config.json
{
"hooks": {
"notify-me": {
"type": "preToolCall",
"visible": false,
"actions": [
{
"type": "shell",
"file": "hooks/my-hook.sh"
}
]
}
}
}
~/.config/eca/config.json
{
"hooks": {
"load-context": {
"type": "chatStart",
"actions": [{
"type": "shell",
"shell": "echo '{\"additionalContext\": \"Today is '$(date +%Y-%m-%d)'\"}'"
}]
}
}
}
~/.config/eca/config.json
{
"hooks": {
"add-prefix": {
"type": "preRequest",
"actions": [{
"type": "shell",
"shell": "jq -c '{replacedPrompt: (\"[IMPORTANT] \" + .prompt)}'"
}]
}
}
}
~/.config/eca/config.json
{
"hooks": {
"block-rm": {
"type": "preToolCall",
"matcher": "eca__shell_command",
"actions": [{
"type": "shell",
"shell": "if jq -e '.tool_input.command | test(\"rm -rf\")' > /dev/null; then echo '{\"approval\":\"deny\",\"additionalContext\":\"Dangerous command blocked\"}'; fi"
}]
}
}
}
~/.config/eca/config.json
{
"hooks": {
"force-recursive": {
"type": "preToolCall",
"matcher": "eca__directory_tree",
"actions": [{
"type": "shell",
"shell": "echo '{\"updatedInput\": {\"max_depth\": 3}}'"
}]
}
}
}
~/.config/eca/config.json
{
"hooks": {
"my-hook": {
"type": "preToolCall",
"actions": [{"type": "shell", "file": "~/.config/eca/hooks/check-tool.sh"}]
}
}
}