QueryMT Agent - Hooks
Hooks let you run configured local commands at specific points in the agent lifecycle. They are useful for policy checks, audit logging, approval automation, ordered tool input and result transformation, non-destructive request projection, compaction control, delegation control, and stop-time validation.
Overview
Hooks are configured at the agent/profile level through [agent.hooks]. A hook command receives JSON on stdin and returns JSON on stdout. QueryMT validates the wire format with generated schemas.
Hooks are a good fit for:
- enforcing local tool policies without recompiling QueryMT
- review or planning profiles with stricter approval rules
- custom shell safety checks
- stop-time verification before a turn ends
- schema-backed lifecycle command hooks
Security Model
Hooks execute arbitrary local commands.
- Hooks are disabled by default.
- Enable hooks only in trusted agent configs or profiles.
- Hook commands receive prompt and tool metadata on stdin.
- For now, QueryMT supports config/profile-level hooks only.
- Automatic
~/.qmt/hooksor project.qmt/hooksdiscovery is not implemented yet.
Configuration
Hooks are configured under [agent.hooks].
Because hooks live in [agent.hooks], they work well with QueryMT profiles. For example, a review profile can enable stricter stop hooks while a coding profile can enable shell-policy hooks. See crates/agent/examples/confs/hook_guarded_coder.toml and the companion scripts in crates/agent/examples/hooks/ for a complete runnable example.
Handlers may also invoke an already-connected MCP tool directly:
MCP hook handlers are bound to both server and tool, bypass the model-visible tool loop, and therefore cannot recursively trigger tool hooks. Template strings must be exact $event.<field> paths into the event payload; arbitrary interpolation is not supported. MCP handlers currently require an execution-scoped event (pre_tool_use, permission_request, post_tool_use, or context) where connected tool state is available. The MCP tool's text content is parsed with the same output contract as a command hook.
Hook Command Protocol
Each command hook:
- runs as a local process
- receives one JSON object on stdin
- returns one JSON object on stdout
- may return an empty JSON object (
{}) when it has no action to take
Common input fields include:
permission_mode is derived from the agent mode captured when the turn starts. If the user changes mode while a turn is running, hooks for that turn continue using the captured mode; the next turn uses the new mode.
Current values are:
default: Build modeplan: Plan modeaccept_edits: Review mode
Events
QueryMT currently supports these hook events:
| Event | Matcher | Effect |
|---|---|---|
session_start |
none | Gate the next run and contribute context to the next user turn |
user_prompt_submit |
none | Block a prompt or persist provenance-labeled context with it |
pre_tool_use |
tool name regex | Block, approve/defer permission, or chain tool-input rewrites |
permission_request |
tool name regex | Allow or deny permission prompts; deny wins |
post_tool_use |
tool name regex | Chain model-visible content/error patches without changing execution facts |
context |
none | Replace the current request projection or add request-only context |
pre_compaction |
none | Block compaction or provide a complete custom summary |
post_compaction |
none | Append context to the stored compaction summary |
pre_delegation |
target agent regex | Block or rewrite a delegation request before it is recorded |
delegation_start |
target agent regex | Observe delegation start and append planning context for the child session |
post_delegation |
target agent regex | Append context to the delegate summary injected back into the planner |
delegation_failure |
target agent regex | Append context to the failure message injected back into the planner |
stop |
none | Request one extra LLM step |
session_end |
none | Observe explicit session close or deletion |
Handlers run in configuration order. Transform-capable events rebuild each handler's input from the last accepted value. A pre-operation block is sticky. Invalid JSON and invalid replacements emit a durable hook_notice and retain the last valid value.
Command exit behavior is:
- exit
0: parse stdout; empty stdout is a no-op - exit
2: block with trimmed stderr as the reason; stdout does not control the operation - other non-zero exits, signals, and timeouts: hook infrastructure errors
system_message: emitted as a non-error durable notice and never inserted into model context
additional_context defaults to an approximate 2500-token limit, configurable per handler with additional_context_limit. Prompt and tool contributions are persisted as typed, provenance-labeled message parts. Tool contributions are placed after every tool_result block in the combined result message. context contributions exist only in the prepared request and never mutate stored history.
Examples
pre_tool_use block
Example script for crates/agent/examples/hooks/check-shell.sh:
Expected hook output:
pre_tool_use rewrite
Multiple matching pre_tool_use handlers see this rewritten object in order. A rewrite is accepted only with permission_decision: "allow"; ask follows normal interactive permission handling.
post_tool_use patch
The tool_response input remains available as flattened text. The new tool_result input contains structured content, model-facing is_error, immutable execution_is_error, and tool_source. Post hooks run before final output truncation.
context projection
A context replacement affects one logical request only. QueryMT rejects malformed projections, unmatched tool calls/results, or changes to existing tool-call identities, then recomputes cache hints after transformation. Provider retries reuse the prepared projection and do not rerun the hook.
permission_request allow
Example script for crates/agent/examples/hooks/approve-safe-shell.sh:
Expected hook output:
custom compaction
A non-blank custom summary skips the compaction model call. Each later handler receives the current candidate in candidate_summary; the last valid candidate wins. pre_compaction.messages contains the effective typed history used for compaction. QueryMT still owns token counts, message IDs, persistence, boundaries, and effective-history reload.
Command hooks are the portable lifecycle mechanism. MCP-backed handlers are intended for automatic retrieval, sanitation, and policy calls against connected services; ordinary MCP tools remain separately registered and model-visible. Compiled middleware remains the right choice for in-process state-machine behavior.
pre_compaction block
When pre_compaction blocks, QueryMT does not start compaction. The hook reason becomes the final stop message shown to the user for that context-threshold stop.
pre_delegation rewrite and block
pre_delegation is the only delegation hook that can change behavior. If it blocks, QueryMT rewrites the delegate tool result so the model sees Delegation blocked by hook: ... instead of the original queued message.
delegation_start planning context
delegation_start is observe-only. Its additional_context is appended to the child session planning context, not used as a control signal.
stop continuation
Example script for crates/agent/examples/hooks/stop-verify.sh:
Expected hook output:
Stop Hook Behavior
stop runs when a turn would normally complete.
If a stop hook returns "continue": false, QueryMT runs one additional LLM step for that turn. QueryMT injects a clearly labeled runtime control message into the next LLM call, wrapped as a <system-reminder> block and marked as generated by the hook runtime rather than by the user.
To avoid runaway loops, QueryMT currently allows at most one stop-hook continuation per turn.
JSON Schemas
Generated schemas are committed in the repository and define the stdin/stdout contract for hook authors. QueryMT's snake_case schemas are the source of truth for this feature.
Relevant files include:
crates/agent/src/hooks/schema/generated/pre-tool-use.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/pre-tool-use.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/permission-request.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/permission-request.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/context.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/context.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/pre-compaction.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/pre-compaction.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/post-compaction.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/post-compaction.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/pre-delegation.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/pre-delegation.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/delegation-start.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/delegation-start.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/post-delegation.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/post-delegation.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/delegation-failure.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/delegation-failure.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/session-end.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/session-end.command.output.schema.jsoncrates/agent/src/hooks/schema/generated/stop.command.input.schema.jsoncrates/agent/src/hooks/schema/generated/stop.command.output.schema.json
Hooks vs Middleware
Hooks and middleware are complementary extension points.
Hooks are profile/config-level command integrations with JSON stdin/stdout contracts. Middleware is compiled Rust code that participates directly in the agent state machine.
A hook policy can often be reimplemented as middleware, but middleware is not a drop-in replacement for hooks. Rewriting a hook as middleware changes how it is authored, distributed, configured, validated, and trusted.
Hook Notices
When a hook exits successfully but returns invalid or non-JSON stdout, QueryMT ignores that hook output for control-flow purposes and emits a durable hook_notice event instead.
hook_notice includes:
event_name: the hook lifecycle event such aspre_tool_useorstopmessage: a human-readable warning describing the invalid hook outputis_error:truewhen the notice represents an error condition
This lets dashboards, session timelines, and event subscribers surface hook problems without breaking the turn.
Current Limitations
- Hooks are configured through agent configs and profiles only.
- Automatic global or project hook discovery is not implemented yet.
additional_contextis injected only for selected events today:stop,post_compaction,delegation_start,post_delegation, anddelegation_failure.- Hook input JSON is produced from typed Rust structs and not runtime-validated against JSON Schema; the generated schemas serve as the documented/tested contract.