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, tool input rewriting, 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.
Hook Command Protocol
Each hook command:
- 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 | Observe session creation |
user_prompt_submit |
none | Block a prompt |
pre_tool_use |
tool name regex | Block or rewrite tool input |
permission_request |
tool name regex | Allow or deny permission prompts |
post_tool_use |
tool name regex | Mark a tool result as blocked/error |
pre_compaction |
none | Block compaction and replace the final stop reason shown to the user |
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 |
Examples
pre_tool_use block
Example script for crates/agent/examples/hooks/check-shell.sh:
Expected hook output:
pre_tool_use rewrite
permission_request allow
Example script for crates/agent/examples/hooks/approve-safe-shell.sh:
Expected hook output:
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/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/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.