How do hooks work?
Based on the codebase, here's how hooks work in cagent:
Hooks are lifecycle events that allow you to run shell commands at various points during agent execution. They provide deterministic control over agent behavior—blocking dangerous operations, modifying inputs, or adding context.
| Event | Trigger | Use Case |
|---|---|---|
pre_tool_use |
Before a tool executes | Allow/deny/modify tool calls |
post_tool_use |
After a tool completes | Validation, feedback, logging |
session_start |
When session begins | Setup environment, load context |
session_end |
When session terminates | Cleanup, persist state |
hooks:
pre_tool_use:
- matcher: "shell|edit_file" # regex pattern, "*" matches all
hooks:
- type: command
command: "your-script.sh"
timeout: 10 # seconds, default 60
post_tool_use:
- matcher: "*"
hooks:
- type: command
command: "echo 'tool finished'"
session_start:
- type: command
command: "echo 'session starting'"
session_end:
- type: command
command: "echo 'goodbye'"Hooks receive JSON via stdin containing context:
{
"session_id": "abc123",
"cwd": "/path/to/workdir",
"hook_event_name": "pre_tool_use",
"tool_name": "shell",
"tool_use_id": "xyz789",
"tool_input": {"cmd": "ls -la"}
}For post_tool_use, it also includes tool_response. For session_start/session_end, includes source or reason.
To control execution, output JSON:
{
"continue": false,
"stop_reason": "Operation blocked",
"decision": "block",
"reason": "Not allowed",
"system_message": "Warning to show user",
"hook_specific_output": {
"permission_decision": "deny",
"permission_decision_reason": "Dangerous command",
"updated_input": {"cmd": "ls -lah"}
}
}- 0: Success, parse JSON output
- 2: Blocking error—operation denied, stderr shown as reason
- Other non-zero: Non-blocking warning, logged but execution continues
- Matcher patterns are compiled as regex (anchored with
^(?:...)$) - Multiple matching hooks execute in parallel
- Results are aggregated—any deny blocks the operation
- Deduplication prevents the same hook command running twice
- Timeouts enforce maximum execution time per hook
hooks:
pre_tool_use:
- matcher: "shell"
hooks:
- type: command
command: |
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.cmd // ""')
if echo "$CMD" | grep -qE 'rm\s+-rf|sudo'; then
echo '{"hook_specific_output":{"permission_decision":"deny","permission_decision_reason":"Blocked dangerous command"}}'
exit 0
fiHooks are configured per-agent in the agent definition and executed by the runtime in pkg/runtime/runtime.go which calls getHooksExecutor() to create a hooks.Executor that handles all hook execution for tool calls.
So can I, in a session hook, add additional data that will be sent to the LLM?
Yes, you can. For session_start and post_tool_use hooks, any output is captured as additional context.
Just output text to stdout—it gets added as additional context:
session_start:
- type: command
command: |
echo "Current git branch: $(git branch --show-current)"
echo "Last commit: $(git log -1 --oneline)"Use the hook_specific_output field for more control:
session_start:
- type: command
command: |
cat <<EOF
{
"hook_specific_output": {
"additional_context": "Project uses Go 1.22. Always run 'task lint' before committing."
}
}
EOFLooking at pkg/hooks/executor.go:
// Additional context
if hso.AdditionalContext != "" {
additionalContexts = append(additionalContexts, hso.AdditionalContext)
}And for plain stdout:
} else if r.stdout != "" {
// Plain text stdout is added as context for some events
if eventType == EventSessionStart || eventType == EventPostToolUse {
additionalContexts = append(additionalContexts, strings.TrimSpace(r.stdout))
}
}The AdditionalContext field in the result is then available to the runtime to include in the LLM conversation.
session_start:
- type: command
command: |
# Gather project context
CONTEXT=""
if [ -f "package.json" ]; then
CONTEXT="$CONTEXT\nNode.js project. Dependencies: $(jq -r '.dependencies | keys | join(", ")' package.json)"
fi
if [ -f "go.mod" ]; then
CONTEXT="$CONTEXT\nGo module: $(head -1 go.mod | cut -d' ' -f2)"
fi
if [ -f ".env.example" ]; then
CONTEXT="$CONTEXT\nRequired env vars: $(grep -v '^#' .env.example | cut -d'=' -f1 | tr '\n' ', ')"
fi
echo -e "$CONTEXT"This context gets injected into the session so the LLM knows about your project setup.