Skip to content

Instantly share code, notes, and snippets.

@rumpl
Last active January 7, 2026 18:48
Show Gist options
  • Select an option

  • Save rumpl/97998d8d08f72655ff4a9b491a31c8c4 to your computer and use it in GitHub Desktop.

Select an option

Save rumpl/97998d8d08f72655ff4a9b491a31c8c4 to your computer and use it in GitHub Desktop.

User

How do hooks work?

Assistant (root)

Based on the codebase, here's how hooks work in cagent:

Hooks Overview

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.

Hook Event Types

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

Configuration Structure

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'"

Hook Input

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.

Hook Output (JSON)

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"}
  }
}

Exit Codes

  • 0: Success, parse JSON output
  • 2: Blocking error—operation denied, stderr shown as reason
  • Other non-zero: Non-blocking warning, logged but execution continues

Execution Details

  1. Matcher patterns are compiled as regex (anchored with ^(?:...)$)
  2. Multiple matching hooks execute in parallel
  3. Results are aggregated—any deny blocks the operation
  4. Deduplication prevents the same hook command running twice
  5. Timeouts enforce maximum execution time per hook

Example: Blocking Dangerous Commands

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
            fi

Integration

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

User

So can I, in a session hook, add additional data that will be sent to the LLM?

Assistant (root)

Yes, you can. For session_start and post_tool_use hooks, any output is captured as additional context.

Two Ways to Add Context

1. Plain Text Output (Simple)

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)"

2. Structured JSON with additional_context

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."
        }
      }
      EOF

How It's Processed

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

Practical Example

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment