Hook contracts and compatibility dispatch.
This package contains two hook systems: Hooks v2 (current) and legacy hooks (deprecated, removal September 1, 2026).
To write a new hook integration, use the v2 config format — see
deepagents_code.hooks.loading for file locations and precedence, and
deepagents_code.hooks.models.config + deepagents_code.hooks.models.wire
for the schema and stdin payload shapes.
deepagents_code.hooks.legacy exists only for backward compatibility; new
integrations should not target it.
Max characters of tool_output included in tool.result hook payloads.
Bounds payload size (data-amplification guard) while keeping enough of the
tool's output to be useful to audit/notification hooks. Applied in the single
shared builder _tool_stream.build_tool_result_payload, which both the
interactive and headless dispatch paths call, so the cap never drifts between
them. Only tool_output is capped; tool_args is passed through in full so hooks
that act on the arguments (e.g. a linter reading a write_file content) see
the exact value the tool received.
Fire matching hook commands with payload serialized as JSON on stdin.
The event name is automatically injected into the payload under the
"event" key so callers don't need to duplicate it.
The blocking subprocess work is offloaded to a thread so the caller's
event loop is never stalled. Matching hooks run concurrently, each bounded
by HOOK_SUBPROCESS_TIMEOUT. Errors are logged and never propagated.
Schedule dispatch_hook as a background task with a strong reference.
Use this instead of bare create_task(dispatch_hook(...)) to prevent the
task from being garbage collected before completion.
Safe to call from sync code as long as an event loop is running.
Await all in-flight fire-and-forget hook tasks.
Call this before the event loop tears down (e.g. at the end of a headless
run driven by asyncio.run) so background dispatches — most importantly the
final tool.result — are not cancelled mid-flight and silently dropped.
Each task's exceptions are already swallowed inside dispatch_hook, and any
stragglers are collected with return_exceptions=True, so this never
raises.
Precondition: this snapshots the in-flight set once and awaits it, so any
hook scheduled after the snapshot (during the await) is not drained. Call
it only once no further dispatches are possible. The headless caller invokes
it after _run_agent_loop has fully returned. The app.py graceful-exit
caller cancels the agent worker first, whose cancel handler
(_handle_interrupt_cleanup) schedules its terminal tool.result hooks
synchronously before this snapshot runs — see the ordering comment there —
so they are captured; a hook scheduled after a slow async write would not
be.
Return whether fire-and-forget hook tasks are still in flight.
Legacy dotted-event migration helpers for Hooks v2 configuration.
Legacy documents are converted by the loader so lifecycle call sites dispatch only canonical events and do not duplicate old dotted-event hooks.
_LEGACY_EVENT_MAP is the authoritative list of legacy events migrated into
Hooks v2. Events absent from it (e.g. permission.request, tool.use,
tool.result) are dropped from the migrated configuration only; they continue
to fire through the legacy dispatcher (deepagents_code.hooks.legacy) until
the legacy system is removed.
Standalone orchestration for the Hooks v2 execution engine.
Single-owner coordinator for client-side Hooks v2 state.
HooksManager is the only place in the client that builds or holds a
HooksRuntime. The Textual app, the Textual stream adapter, and the headless
runner each hold a manager and call intention-revealing lifecycle methods on
it; none of them inspect the runtime, the hook service, or their availability.
A manager whose configuration failed to load stays usable and answers every call with a neutral result, so consumers never need an availability check.
Client-owned conversation transcript projections for Hooks v2.
Materializes versioned per-thread and per-subagent JSONL files that hook
commands can read via transcript_path / agent_transcript_path.
Canonical boundary between hook domain and wire models.
Bounded asynchronous command execution for Hooks v2.
Persistent workspace trust for project-scoped hooks.
Immutable runtime snapshots for Hooks v2 configuration.
Capability registry for Hooks v2 events.
Session-scoped client facade for the Hooks v2 runtime.
Event-aware reduction for Hooks v2 command output.
Client-side fulfillment for server-owned Hooks v2 interrupts.
Projection from Hooks v2 domain invocations to compatible wire input.
Sanitized subprocess environments for Hooks v2 command handlers.
Native dcode tool vocabulary mapped to compatible wire names.
Client↔server interrupt transport for Hooks v2 server-owned events.
Helpers for attaching Hooks v2 session identity to graph context.
User-facing presentation for Hooks v2 execution.
HookPresenter is the single place that turns hook results into something a
person sees. It is owned by HooksManager, handed to every runtime that
manager loads, and kept alive across reloads so its output sinks can be
rebound once a UI exists without any other object holding its own copy.
Terminal escape-sequence validation for hook output.
Client-owned Hooks v2 lifecycle facade.
Translation between PermissionRequest decisions and HITL review payloads.
Shared by the Textual and headless approval paths so both surfaces resolve hook-driven permission decisions identically.
Server-owned Hooks v2 lifecycle middleware.
Emits PreCompact, PreToolUse, PostToolUse, PostToolUseFailure, Stop,
SubagentStart, and SubagentStop through the LangGraph interrupt channel so the
client runtime can execute matching handlers and return typed decisions.
Typed contracts for the hooks system.
Lightweight hook dispatch for external tool integration.
DEPRECATED: This is the legacy hook system, kept for backward compatibility
until September 1, 2026. New integrations should use Hooks v2 — see
deepagents_code.hooks.loading for config locations and
deepagents_code.hooks.models for the schema. Legacy documents are migrated
to v2 at load time (deepagents_code.hooks.migration).
Loads hook configuration from ~/.deepagents/hooks.json and fires matching
commands with JSON payloads on stdin. Subprocess work is offloaded to a
background thread so the caller's event loop is never stalled. Failures are
logged but never bubble up to the caller.
Config format (~/.deepagents/hooks.json):
{"hooks": [{"command": ["bash", "adapter.sh"], "events": ["session.start"]}]}
If events is omitted or empty the hook receives all events.
Onboarding emits user.name.set with {"name": "...", "assistant_id": "..."}
after the user submits a non-empty preferred name.
tool.use fires before a tool call once its streamed arguments parse into a
complete value and its tool-call id is known; a call whose arguments never
parse, or that carries no id, is skipped. tool.result fires after every tool
call reaches a terminal state — successful execution, failure, or HITL
rejection/cancellation. The three blocks below show the payload shapes, not a
single sequence of events:
{"event": "tool.use", "tool_name": "write_file", "tool_id": "toolu_abc123",
"tool_args": {"file_path": "src/foo.py", "content": "..."}}
{"event": "tool.result", "tool_name": "write_file", "tool_id": "toolu_abc123",
"tool_args": {"file_path": "src/foo.py", "content": "..."},
"tool_status": "success", "tool_output": "Updated file src/foo.py"}
{"event": "tool.error", "tool_names": ["write_file"]}
tool_args is the parsed tool-call arguments; a non-object value (rare) is
wrapped as {"value": ...}. tool_output is the tool's returned content,
capped to HOOK_TOOL_OUTPUT_LIMIT characters (tool_args is not truncated); a
capped value ends with …[output truncated] so a consumer can tell a truncated
result from a short one.
tool_status is "success" or "error"; "error" covers both a tool that
raised and a call the user rejected or cancelled. Whenever a tool.result has
tool_status: "error", tool.error (payload {"tool_names": [<name>]}) fires
alongside it, so existing tool.error hooks are unaffected.
tool_args is {} whenever a tool.result cannot be correlated back to a
tool.use — either because the call carried no id (then tool_id is null) or
because no tool.use fired for it (e.g. its args never parsed), in which case
tool_id may still be the real string id.
Ordering: the tool events (tool.use, tool.result, tool.error) are
dispatched fire-and-forget (see dispatch_hook_fire_and_forget) and every
matching hook command runs in its own subprocess. A tool.use is dispatched
before its tool.result, but the two run concurrently, so a hook subscribed to
both may observe them out of order, and events from parallel tool calls
interleave freely. Correlate by tool_id rather than relying on arrival order —
there is no cross-event delivery-ordering guarantee for the tool events. Most
non-tool events (session.start, task.complete, session.end, user.prompt,
context.offload, context.compact, permission.request) fire in program order.
They are dispatched with an awaited dispatch_hook, except session.end on the
interactive TUI, which is dispatched via _dispatch_hook_sync at shutdown. That
dispatch runs on a worker thread (asyncio.to_thread) inside the coordinated
teardown in app.py so a slow hook can't block rendering or delay agent
cancellation — it overlaps agent cleanup and server shutdown, and teardown awaits
it before stopping the event loop, so it is dispatched once (never duplicated) and
after every prior non-tool event; the program-order guarantee therefore holds.
Delivery is at-most-once: a force-quit second exit can stop the loop before the
dispatch completes and drop it.
input.required and
user.name.set are the exceptions with no program-order guarantee:
user.name.set is always dispatched fire-and-forget, and input.required is
fire-and-forget on the headless surface (awaited only in the interactive TUI).
Validated Hooks v2 configuration loading, merging, and hashing.
Precedence (highest first, earlier in reduction order):
{project_root}/.deepagents/hooks.json~/.deepagents/hooks.json (or config_dir/hooks.json in tests)hooks.json documents contributed by enabled pluginsSources are concatenated per event. Precedence is reduction order, not execution order: every matching handler runs, and the first one that stops processing decides the event.
A minimal v2 config with one event and one command handler:
{"hooks": {"Notification": [{"matcher": "agent_completed",
"hooks": [{"type": "command", "command": "bash notify.sh"}]}]}}
Hook stdin payloads are the Claude-compatible envelope (hook_event_name +
session_id + event-specific fields), defined in
deepagents_code.hooks.models.wire.
Legacy list-shaped documents are migrated only for events whose lifecycle semantics genuinely match Hooks v2.