Hooks allow executing shell commands or Python handlers on tool lifecycle events, following Claude Code's hook conventions. Use them for audit logging, security gates, content filtering, or custom side effects.
from pydantic_deep import create_deep_agent, Hook, HookEvent
agent = create_deep_agent(
hooks=[
Hook(
event=HookEvent.PRE_TOOL_USE,
command="my-security-checker",
matcher="execute|write_file",
),
],
)| Event | When | Can Modify |
|---|---|---|
PRE_TOOL_USE |
Before tool execution | Deny tool call, modify args |
POST_TOOL_USE |
After successful tool call | Modify result |
POST_TOOL_USE_FAILURE |
After a tool call fails | Observe only |
Command hooks run shell commands via SandboxProtocol.execute(). The hook receives HookInput as JSON on stdin and uses exit codes for decisions:
- Exit 0 — Allow (optionally return JSON with modifications)
- Exit 2 — Deny (stdout becomes the denial reason)
Hook(
event=HookEvent.PRE_TOOL_USE,
command="python scripts/security_check.py",
matcher="execute", # Only match the execute tool
timeout=30, # Seconds (default: 30)
)!!! warning "Command hooks require a SandboxProtocol backend"
Use LocalBackend or DockerSandbox. StateBackend does not support execute().
Handler hooks call async Python functions directly:
from pydantic_deep.middleware.hooks import HookInput, HookResult
async def audit_logger(hook_input: HookInput) -> HookResult:
print(f"[AUDIT] {hook_input.tool_name}({hook_input.tool_input})")
return HookResult(allow=True)
async def safety_gate(hook_input: HookInput) -> HookResult:
if "rm -rf" in str(hook_input.tool_input):
return HookResult(allow=False, reason="Dangerous command blocked")
return HookResult(allow=True)
agent = create_deep_agent(
hooks=[
Hook(event=HookEvent.POST_TOOL_USE, handler=audit_logger),
Hook(event=HookEvent.PRE_TOOL_USE, handler=safety_gate, matcher="execute"),
],
)The matcher field is a regex pattern matched against the tool name. None matches all tools.
# Match a specific tool
Hook(event=HookEvent.PRE_TOOL_USE, handler=my_handler, matcher="execute")
# Match multiple tools
Hook(event=HookEvent.PRE_TOOL_USE, handler=my_handler, matcher="execute|write_file|edit_file")
# Match all tools (default)
Hook(event=HookEvent.POST_TOOL_USE, handler=audit_logger, matcher=None)Command hooks can modify tool arguments by printing JSON to stdout:
{"modified_args": {"path": "/safe/path/file.txt"}}Python handlers return modifications via HookResult:
async def normalize_paths(hook_input: HookInput) -> HookResult:
args = dict(hook_input.tool_input)
if "path" in args:
args["path"] = args["path"].replace("../", "")
return HookResult(allow=True, modified_args=args)async def redact_secrets(hook_input: HookInput) -> HookResult:
result = hook_input.tool_result or ""
redacted = result.replace("SECRET_KEY=abc123", "SECRET_KEY=***")
return HookResult(modified_result=redacted)
Hook(event=HookEvent.POST_TOOL_USE, handler=redact_secrets, matcher="read_file")Background hooks run as fire-and-forget tasks — they don't block tool execution:
Hook(
event=HookEvent.POST_TOOL_USE,
handler=send_to_analytics,
background=True, # Non-blocking
)Hooks receive this data:
| Field | Type | Description |
|---|---|---|
event |
str |
Event name ("pre_tool_use", "post_tool_use", "post_tool_use_failure") |
tool_name |
str |
Name of the tool being called |
tool_input |
dict |
Tool arguments |
tool_result |
str | None |
Tool output (only for POST events) |
tool_error |
str | None |
Error message (only for POST_TOOL_USE_FAILURE) |
For PRE_TOOL_USE, hooks run in order and first deny wins:
- All matching hooks execute sequentially
- If any hook returns
allow=False, the tool call is denied - Argument modifications accumulate across hooks
For POST_TOOL_USE and POST_TOOL_USE_FAILURE, all matching hooks run:
- Result modifications from each hook are passed to the next
- Background hooks run in parallel without blocking
| Component | Description |
|---|---|
[Hook][pydantic_deep.middleware.hooks.Hook] |
Hook definition (event, command/handler, matcher) |
[HookEvent][pydantic_deep.middleware.hooks.HookEvent] |
Enum: PRE_TOOL_USE, POST_TOOL_USE, POST_TOOL_USE_FAILURE |
[HookInput][pydantic_deep.middleware.hooks.HookInput] |
Data passed to hooks |
[HookResult][pydantic_deep.middleware.hooks.HookResult] |
Result from hook execution |
[HooksMiddleware][pydantic_deep.middleware.hooks.HooksMiddleware] |
Middleware that dispatches hooks |
EXIT_ALLOW |
Exit code 0 — allow |
EXIT_DENY |
Exit code 2 — deny |
- Checkpointing — Save and rewind conversation state
- Memory — Persistent agent memory
- Human-in-the-Loop — Approval workflows