Skip to content

Hooks API

Claude Code-style lifecycle hooks run shell commands or Python handlers around tool execution. Attach them via the hooks parameter on create_deep_agent. See Hooks for the conceptual overview.

Hook

pydantic_deep.capabilities.hooks.Hook dataclass

A hook definition that fires on tool lifecycle events.

Either command or handler must be provided (not both). Command hooks run shell commands via SandboxProtocol.execute(). Handler hooks call async Python functions.

Parameters:

Name Type Description Default
event HookEvent

Which lifecycle event triggers this hook.

required
command str | None

Shell command to execute (receives HookInput JSON via stdin).

None
handler Callable[[HookInput], Awaitable[HookResult]] | None

Async Python function (HookInput) -> HookResult.

None
matcher str | None

Regex pattern matched against tool_name. None matches all tools.

None
timeout int

Command execution timeout in seconds.

30
background bool

If True, hook runs as fire-and-forget (non-blocking).

False

HookEvent

pydantic_deep.capabilities.hooks.HookEvent

Bases: str, Enum

Hook lifecycle events.

Tool events follow Claude Code conventions. Run and model events map to pydantic-ai's AbstractCapability lifecycle hooks.

HookInput

pydantic_deep.capabilities.hooks.HookInput dataclass

Data passed to hook commands/handlers as JSON.

Command hooks receive this as JSON via stdin piping. Python handlers receive this as a dataclass instance.

HookResult

pydantic_deep.capabilities.hooks.HookResult dataclass

Result from a hook execution.

For PRE_TOOL_USE: allow=False denies the tool call. For POST_TOOL_USE: modified_result replaces the tool output.

HooksCapability

pydantic_deep.capabilities.hooks.HooksCapability dataclass

Bases: AbstractCapability[Any]

Capability that executes hooks on tool lifecycle events.

Maps tool events to shell commands (via execute()) or Python handlers, following Claude Code's hook conventions: - PRE_TOOL_USE: before tool execution, can deny - POST_TOOL_USE: after successful tool execution - POST_TOOL_USE_FAILURE: after failed tool execution

before_tool_execute(ctx, *, call, tool_def, args) async

Run PRE_TOOL_USE hooks. First deny raises SkipToolExecution.

after_tool_execute(ctx, *, call, tool_def, args, result) async

Run POST_TOOL_USE hooks. Can modify result.

on_tool_execute_error(ctx, *, call, tool_def, args, error) async

Run POST_TOOL_USE_FAILURE hooks.

before_run(ctx) async

Run BEFORE_RUN hooks at the start of agent.run().

after_run(ctx, *, result) async

Run AFTER_RUN hooks at the end of agent.run().

on_run_error(ctx, *, error) async

Run RUN_ERROR hooks when agent.run() fails.

before_model_request(ctx, request_context) async

Run BEFORE_MODEL_REQUEST hooks before each LLM call.

after_model_request(ctx, *, request_context, response) async

Run AFTER_MODEL_REQUEST hooks after each LLM response.

dispatch_model_fallback(primary, fallback, error, backend) async

Dispatch MODEL_FALLBACK_TRIGGERED hooks outside the normal capability lifecycle.

Called by the fallback_on handler in create_deep_agent when FallbackModel switches from the primary to a fallback model.

default_security_hook

pydantic_deep.capabilities.hooks.default_security_hook(*, blocked_commands=None, allowed_write_roots=None, blocked_write_paths=None, blocked_read_paths=None, redact_secrets=True, secret_patterns=None, mode='deny')

Return a ready-to-use list of security hooks for create_deep_agent.

The returned hooks gate execute, write_file, edit_file, and read_file against destructive command patterns, path-traversal writes, sensitive-path writes, and sensitive-path reads. When redact_secrets is enabled, a second hook scrubs obvious secret shapes (AWS access keys, GitHub PATs, OpenAI sk- keys, JWTs) from POST_TOOL_USE output.

The defaults are opt-out: pass mode="warn" to log instead of block, pass blocked_commands=[] to disable a category, or extend any list with your own patterns. Lists you pass replace the defaults - concatenate with DEFAULT_BLOCKED_COMMANDS etc. if you want to keep them.

Parameters:

Name Type Description Default
blocked_commands Sequence[str] | None

Regex patterns matched against the command arg of the execute tool. Defaults to DEFAULT_BLOCKED_COMMANDS.

None
allowed_write_roots Sequence[str | Path] | None

If set, write_file/edit_file paths must resolve under one of these roots. Paths must be absolute (no ~ or relative segments) - ~ expands against the controller HOME, which may differ from the agent backend's filesystem namespace (e.g. DockerSandbox). Path-traversal (..) segments are blocked unconditionally regardless of this setting.

None
blocked_write_paths Sequence[str] | None

Regex patterns matched against the path arg of write_file/edit_file. Defaults to DEFAULT_BLOCKED_WRITE_PATHS. Applied unconditionally, independent of allowed_write_roots.

None
blocked_read_paths Sequence[str] | None

Regex patterns matched against the path arg of read_file. Defaults to DEFAULT_BLOCKED_READ_PATHS.

None
redact_secrets bool

When True (default), add a POST_TOOL_USE hook that replaces matches of secret_patterns with [REDACTED] in tool output strings.

True
secret_patterns Sequence[str] | None

Regex patterns redacted from tool output. Defaults to DEFAULT_SECRET_PATTERNS.

None
mode Literal['deny', 'warn']

"deny" (default) blocks matching calls via HookResult(allow=False). "warn" allows them through but logs a warning - useful for shadow-mode rollout before enforcing.

'deny'

Returns:

Type Description
list[Hook]

A list of Hook instances. Pass it straight to create_deep_agent:

list[Hook]

hooks=default_security_hook(). Use

list[Hook]

hooks=[*default_security_hook(), my_extra] to add custom hooks.

Default Blocklists

pydantic_deep.capabilities.hooks.DEFAULT_BLOCKED_COMMANDS = ('\\brm\\s+-[a-zA-Z]*[rR][a-zA-Z]*[fF][a-zA-Z]*\\s+(?:/\\*?/?|~/?|\\$\\{?HOME\\}?/?|\\./?)(?:\\s|$)', '\\brm\\s+-[a-zA-Z]*[fF][a-zA-Z]*[rR][a-zA-Z]*\\s+(?:/\\*?/?|~/?|\\$\\{?HOME\\}?/?|\\./?)(?:\\s|$)', '\\brm\\s+--recursive\\s+--force\\s+(?:/\\*?/?|~/?|\\$\\{?HOME\\}?/?|\\./?)(?:\\s|$)', '\\brm\\s+--force\\s+--recursive\\s+(?:/\\*?/?|~/?|\\$\\{?HOME\\}?/?|\\./?)(?:\\s|$)', ':\\(\\)\\s*\\{\\s*:\\s*\\|\\s*:\\s*&\\s*\\}\\s*;\\s*:', '\\bmkfs(?:\\.\\w+)?\\b', '\\bdd\\s+[^\\n;|&]*\\bof=/dev/(?!null|zero|random|urandom|full|tty|std)', '\\b(?:curl|wget)\\s+[^\\n;|&]*\\|\\s*(?:sh|bash|zsh|ksh|dash)\\b') module-attribute

pydantic_deep.capabilities.hooks.DEFAULT_BLOCKED_READ_PATHS = ('(?:^|/)etc/shadow\\b', '(?:^|/|~/)\\.ssh(?:/|$)', '(?:^|/)\\.env(?:\\.[\\w.-]+)?$', '(?:^|/|~/)\\.aws/credentials\\b', '(?:^|/|~/)\\.config/gcloud(?:/|$)', 'application_default_credentials\\.json$') module-attribute

pydantic_deep.capabilities.hooks.DEFAULT_BLOCKED_WRITE_PATHS = ('(?:^|/|~/)\\.ssh(?:/|$)', '(?:^|/)etc/(?:passwd|shadow|sudoers)\\b', '(?:^|/)etc/cron', '(?:^|/|~/)\\.aws/credentials\\b', '(?:^|/)\\.env(?:\\.[\\w.-]+)?$', '(?:^|/|~/)\\.(?:bash_profile|bashrc|profile|zshrc|zprofile)\\b') module-attribute

pydantic_deep.capabilities.hooks.DEFAULT_SECRET_PATTERNS = ('AKIA[0-9A-Z]{16}', '\\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}|\\bsk-[A-Za-z0-9]{20,}', 'ghp_[A-Za-z0-9]{36}', 'github_pat_[A-Za-z0-9_]{22,}', 'eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+') module-attribute