Skip to main content
Hooks let external programs react to Deep Agents Code lifecycle events. Configure commands in ~/.deepagents/hooks.json and it pipes a JSON payload to each matching command’s stdin whenever an event fires. Hooks run fire-and-forget in a background thread. They never block Deep Agents Code and failures are logged without interrupting your session.

Setup

Create ~/.deepagents/hooks.json:
{
  "hooks": [
    {
      "command": ["bash", "-c", "cat >> ~/deepagents-events.log"],
      "events": ["session.start", "session.end"]
    }
  ]
}
Now every time a session starts or ends, Deep Agents Code appends the event payload to ~/deepagents-events.log.

Hook configuration

The config file contains a single hooks array. Each entry has:
command
list[str]
required
Command and arguments to run. No shell expansion: use ["bash", "-c", "..."] if needed.
events
list[str]
optional
Event names to subscribe to. Omit or leave empty to receive all events.
{
  "hooks": [
    {
      "command": ["python3", "my_handler.py"],
      "events": ["session.start", "task.complete"]
    },
    {
      "command": ["bash", "log_everything.sh"]
    }
  ]
}
The second hook above has no events filter, so it receives every event Deep Agents Code emits.

Payload format

Each hook command receives a JSON object on stdin with an "event" key plus event-specific fields:
{
  "event": "session.start",
  "thread_id": "abc123"
}

Events reference

session.start

Fired when an agent session begins (both interactive and non-interactive modes).
thread_id
string
required
The session thread identifier.

session.end

Fired when a session exits.
thread_id
string
required
The session thread identifier.

user.prompt

Fired in interactive mode when the user submits a chat message. No additional fields.

input.required

Fired when the agent requires human input (human-in-the-loop interrupt). No additional fields.

permission.request

Fired before the approval dialog when one or more tool calls need user permission.
tool_names
list[str]
required
Names of the tools requesting approval.

tool.error

Fired when a tool call returns an error.
tool_names
list[str]
required
Names of the tool(s) that errored.

task.complete

Fired when the agent finishes its current task (the streaming loop ends without further interrupts).
thread_id
string
required
The session thread identifier.

context.compact

Fired before Deep Agents Code compacts (summarizes) the conversation context. No additional fields.

Execution model

  • Background thread: Hook subprocesses run in a thread via asyncio.to_thread so the main event loop is never blocked.
  • Concurrent dispatch: When multiple hooks match an event, they run concurrently in a thread pool.
  • 5-second timeout: Each command has a 5-second timeout. Commands that exceed this are killed.
  • Fire-and-forget: Errors are caught per-hook and logged at debug/warning level. A failing hook never crashes or stalls Deep Agents Code.
  • Lazy loading: The config file is read once on the first event dispatch and cached for the rest of the session.
  • No shell expansion: Commands are executed directly (not through a shell). Wrap in ["bash", "-c", "..."] if you need shell features like pipes or variable expansion.

Hook examples

{
  "hooks": [
    {
      "command": ["bash", "-c", "jq -c . >> ~/.deepagents/hook-events.jsonl"],
      "events": []
    }
  ]
}
{
  "hooks": [
    {
      "command": [
        "bash", "-c",
        "osascript -e 'display notification \"Agent finished\" with title \"Deep Agents\"'"
      ],
      "events": ["task.complete"]
    }
  ]
}
Write a handler script that reads the JSON payload from stdin:
my_handler.py
import json
import sys


def handle_hook_payload(payload: dict) -> None:
    event = payload["event"]
    if event == "session.start":
        print(f"Session started: {payload['thread_id']}", file=sys.stderr)
    elif event == "permission.request":
        print(f"Approval needed for: {payload['tool_names']}", file=sys.stderr)


if __name__ == "__main__":
    handle_hook_payload(json.load(sys.stdin))
~/.deepagents/hooks.json
{
  "hooks": [
    {
      "command": ["python3", "my_handler.py"],
      "events": ["session.start", "permission.request"]
    }
  ]
}

Security considerations

Hooks follow the same trust model as Git hooks or shell aliases — any user who can write to ~/.deepagents/hooks.json can execute arbitrary commands. This is by design:
  • No command injection: Payload data flows only to stdin as JSON, never to command-line arguments. json.dumps handles escaping.
  • No shell by default: Commands run with shell=False, preventing shell injection.
  • Malformed config: Invalid JSON or unexpected types produce logged warnings, not security issues.
Only add hooks from sources you trust. A hook has the same permissions as your user account.

See also