Agent Hooks
- Enforce quality gates on agent responses before they reach users
- Audit and control tool usage with custom bash or Python scripts
- Block operations that match your policy rules before they execute
- Prevent early task completion by validating agent output meets your criteria
The problem
Your agent executes tasks autonomously, investigating incidents, running tools, generating responses. But autonomy without oversight creates risk:
- Incomplete responses: The agent says "done" before addressing everything you asked for
- Unaudited tool usage: You have no visibility into which tools the agent calls or what results it gets
- No policy enforcement: Dangerous operations (destructive commands, unauthorized changes) proceed unchecked
- Quality gaps: Responses miss critical information because there's no validation step
You need a way to intercept agent behavior at key moments, without slowing it down or removing its autonomy entirely.
How agent hooks work
Hooks are custom checkpoints you attach to specific agent events. When an event fires, your hook evaluates the situation and decides whether to allow or block the action.
New thread starts → Start hook fires → Inject context
Agent about to call → PreToolUse hook checks the call → Allow, deny, ask, or override policy
Agent used a tool → PostToolUse hook checks result → Allow, block, or inject context
Agent about to stop → Stop hook evaluates response → Allow or reject
Where hooks can be configured
Hooks operate at three scopes:
| Scope | Where to configure | Who can configure | Applies to |
|---|---|---|---|
| Agent level | Builder → Hooks in the portal | SRE Agent Administrator | The main agent and all its threads. These hooks gate child launches but are not copied into child loops. |
| Custom agent level | Agent Canvas → Custom agent → Manage Hooks, or via the REST API v2 | SRE Agent Administrator | Whenever that custom agent runs, including as a child. |
| User-global | Deployable kind: Hook YAML via the REST API v2 | SRE Agent Administrator | PreToolUse and PostToolUse run in parent and child loops; Start and Stop run only in parent loops. |
Child loops combine inherited global tool-event hooks with hooks configured directly on the child. They do not inherit the parent agent's agent-level hooks.
All three can coexist. Matching hooks are evaluated in tier order — system-global → agent → user-global — until one returns deny or ask, which ends the chain immediately. A hook in an earlier tier can therefore prevent a later one from running at all, so do not rely on a user-global hook as a final backstop. See Ordering and Aggregation for how results combine, and Configuration for the user-global YAML shape.
Children inherit the parent thread's effective run mode and global/system PreToolUse and PostToolUse hooks. They also run hooks configured directly on the child, including child-owned Start and Stop hooks. Global/system Start and Stop hooks are not inherited.
A child cannot suspend to ask the user for approval. A PreToolUse ask blocks
the command before execution. A PostToolUse ask occurs after execution, so
the runtime withholds the original output and tells the child to try another
approach. See
Child inheritance and user interaction.
Four hook events are supported:
| Event | Triggers when | You can | Configure via |
|---|---|---|---|
| Start | A new thread begins (first message) | Inject context (command hooks), filter by thread source | API / YAML |
| PreToolUse | Agent is about to execute a tool | Allow, deny, or ask; inject context; override tool access policies | API / YAML |
| PostToolUse | A tool finishes executing | Audit usage, block results, inject additional context | Portal, API / YAML |
| Stop | Agent is about to return a final response | Validate completeness, reject and force the agent to continue | Portal, API / YAML |
Start and PreToolUse hooks are configured via the REST API v2 or YAML. The portal UI currently supports PostToolUse and Stop hooks.
Two execution types
You can implement hooks using either an LLM or a shell script:
| Type | How it works | Best for |
|---|---|---|
| Prompt | An LLM evaluates your prompt and returns a JSON decision | Nuanced validation ("Is this response complete?") |
| Command | A bash or Python script runs in a sandboxed environment | Deterministic checks, policy enforcement, auditing |
Prompt hooks are powerful for subjective evaluation, checking if a response addresses all user concerns or verifying that an investigation was thorough enough. They use the $ARGUMENTS placeholder to receive the full hook context. If $ARGUMENTS is not present in the prompt, the context is appended automatically. Prompt hooks also receive ReadFile and GrepSearch tools when a conversation transcript is available, allowing the LLM to reason about the full conversation history.
Command hooks are better for deterministic checks: validating that a response contains required markers, blocking dangerous commands, or logging tool usage to an external system.
Hooks complement run mode safety controls and tool access policies. Run modes control what the agent can do. Policies control which tools. Hooks control how well it does it and what happens with the results.
Before and after
| Before | After | |
|---|---|---|
| Response quality | Agent stops when it thinks it's done | Your Stop hook validates completeness before the response reaches users |
| Tool visibility | No audit trail of tool execution | PostToolUse hooks log and verify matching parent-loop tool calls |
| Policy enforcement | Dangerous commands execute unchecked | PreToolUse scripts block rm -rf, sudo, and other risky patterns before they run |
| Quality assurance | Prompt engineering is your only lever | LLM-based hooks evaluate nuance; scripts enforce deterministic rules |
How to configure hooks
The easiest way to create hooks is through the portal UI:
- Agent-level hooks: Go to Builder → Hooks → click Create hook
- Custom-agent-level hooks: Go to Agent Canvas → click a custom agent → Manage Hooks
See the Create Hooks via Portal tutorial for step-by-step instructions.
Hooks can also be configured via the REST API v2 using PUT /api/v2/extendedAgent/agents/{agentName}. The YAML format below shows the full configuration schema. See the API tutorial for details.
Note: The Agent Canvas YAML tab displays v1 format and does not show hooks. Use the Hooks page under Builder to view and manage hooks.
api_version: azuresre.ai/v2
kind: ExtendedAgent
metadata:
name: my_hooked_agent
spec:
instructions: |
You are a helpful assistant.
handoffDescription: ""
enableVanillaMode: true
hooks:
Stop:
- type: prompt
prompt: |
Check if the response ends with "Task complete."
$ARGUMENTS
Respond with:
- {"ok": true} if it does
- {"ok": false, "reason": "End your response with 'Task complete.'"} if not
timeout: 30
PreToolUse:
- type: command
matcher: "RunInTerminal|RunAzCliWriteCommands"
timeout: 30
failMode: block
script: |
#!/usr/bin/env python3
import sys, json, re
context = json.load(sys.stdin)
tool_input = context.get('tool_input')
if not isinstance(tool_input, dict):
tool_input = {}
command = tool_input.get('command', '')
dangerous = [r'\brm\s+-rf\b', r'\bsudo\b', r'\bchmod\s+777\b']
for pattern in dangerous:
if re.search(pattern, command):
print(json.dumps({
"ok": False,
"reason": f"Blocked: {pattern}",
"hookSpecificOutput": {"permissionDecision": "deny"}
}))
sys.exit(0)
print(json.dumps({"ok": True}))
This policy must run on PreToolUse. A PostToolUse hook fires after the tool
has already run, so it can block or flag the result but cannot stop rm -rf
from deleting anything. It also cannot rewrite the result — blocking replaces it
wholesale with the hook's reason. Use PostToolUse for auditing, not for
prevention.
The pattern list is an illustrative heuristic, not a security boundary. A
blocklist only catches the spellings you thought of — rm -r -f, an aliased or
interpolated command, or an equivalent call through another tool all slip
through. When deployed as a user-global hook, it also sees commands selected by
Task- and Agent-launched children. A hook configured only on the parent agent
sees the child launch but is not copied into the child loop. Use the blocklist
to catch honest mistakes, and rely on
tool access policies and
run modes for enforcement that has to hold.
Hook response format
Hooks must output JSON with an ok field carrying the decision. Both prompt and
command hooks return ok and reason:
{"ok": true}
{"ok": false, "reason": "Please include more details."}
Command hooks can also return hookSpecificOutput, which carries extras such as
additionalContext and permission decisions:
{"ok": true, "hookSpecificOutput": {"additionalContext": "Tool audit logged."}}
Prompt hooks return only ok and reason — they cannot return
hookSpecificOutput, additionalContext, or a permission decision, and any
that are present are ignored rather than rejected. Two consequences follow. A
response with no boolean ok is malformed and the action is allowed. And
when ok is present it decides alone, so a permission decision copied into a
prompt hook is governed by its ok value: an ask-shaped response (ok: true)
passes without pausing, and a hard-deny response (ok: false) degrades to an
ordinary soft rejection.
Command hooks also still accept a legacy decision field:
{"decision": "block", "reason": "Dangerous command detected."}
Only "block" is meaningful — every other value passes. When decision is
present it overrides ok, so don't set both. Prefer ok in new hooks.
Prompt hooks cannot use decision. A prompt-hook response without a
boolean ok is malformed and fails open, so {"decision": "block"} in a
prompt hook allows the action instead of blocking it. Prompt hooks must return
{"ok": false, "reason": "..."}.
Command hooks can also use exit codes instead of JSON output:
| Exit code | Behavior |
|---|---|
0 with no output | Allow (no objection) |
0 with JSON | Parse JSON for decision |
2 | Block — stderr becomes the reason. Ignored on Start. |
| Other | Uses failMode setting (allow or block) |
A Stop hook that rejects with a missing reason keeps the agent running — both hook types substitute a placeholder, Blocked by hook for command hooks and a generic sentence for prompt hooks — so the rejection costs a turn but tells the agent nothing. A whitespace-only reason on a command hook behaves differently: it is dropped rather than replaced, and the agent stops as if the hook had passed. That is a known defect, tracked internally. Always send a non-whitespace reason that says what to do next.
Command Stop rejections never increment stop_rejection_count, so maxRejections does not bound them. A command Stop hook that rejects unconditionally will loop until the turn budget runs out — see Require a section before the run ends for a self-bounding example.
You can define multiple hooks for the same event. For PostToolUse, every hook whose matcher pattern matches is evaluated in tier order, but a deny or an ask ends the chain — hooks after it do not run. If multiple hooks that did run provide additionalContext, the last one's context is injected into the conversation.
Configuration reference
| Option | Type | Default | Description |
|---|---|---|---|
type | string | — | Required. prompt or command. There is no default — validation rejects a hook that omits it |
prompt | string | — | LLM prompt text (required for prompt hooks). Use $ARGUMENTS for context injection |
command | string | — | Inline shell command (for command hooks, mutually exclusive with script) |
script | string | — | Multi-line script (for command hooks, mutually exclusive with command) |
matcher | string | — | Regex pattern for runtime tool names. Validation requires it only for PostToolUse, but a PreToolUse hook saved without one matches nothing and never fires — set it for both tool events. * matches all tools. Patterns are anchored as ^(pattern)$ and matched case-sensitively. Use actual runtime tool names (e.g., RunInTerminal, RunAzCliWriteCommands) — see tool access policies for the full list. Empty or null matches nothing. |
timeout | int | 30 | Execution timeout in seconds. Must be positive. Agent descriptor validation also rejects values above 300, but hooks created through the API — including global hooks — are not capped, so keep them short regardless |
failMode | string | allow | How to handle hook errors: allow (tool proceeds) or block (tool is denied). Use block for security-critical hooks — allow means a hook crash silently removes the guardrail. |
model | string | ReasoningFast | Model for prompt hooks (scenario name or deployment name) |
maxRejections | int | 3 (agent default) | Max rejections before forcing stop. Range: 1–25. Applies to prompt-type Stop hooks only — command-type Stop hooks have no implicit limit. When multiple prompt hooks specify different values, the maximum is used. |
sources | list | — | Thread source filter for Start hooks. Valid values: Conversation, Alert, Incident, ScheduledTask, Teams, HttpTrigger, Playground, and others. When omitted, the hook fires for all thread types. |
Hook context schema
Hooks receive structured JSON context about the current event. Prompt hooks receive it via the $ARGUMENTS placeholder in the prompt text. Command hooks receive it as JSON on stdin.
The samples below show the common shape. For the complete field-by-field contract — every key, its type, what is not exposed, and how to find real values in a trace — see the Hook Data Contract.
For both hook types, the execution_summary field contains a file path to the conversation transcript (not inline content). For prompt hooks, the LLM receives ReadFile and GrepSearch tools to access this file. Those tools are pointed at the transcript by instruction, not confined to it — a prompt hook can read other workspace files it can reach, so treat this as convenience rather than a confidentiality boundary. For command hooks, the file is available at the specified path in the sandbox. If you configure more than one hook on the same event, only the first one to run sees the transcript — see execution_summary in chained hooks.
Common fields
{
"hook_event_name": "Stop",
"agent_name": "my_agent",
"current_turn": 5,
"max_turns": 50,
"execution_summary": "/path/to/transcript.txt"
}
Stop hook fields
{
"final_output": "Here is my response...",
"stop_hook_active": false,
"stop_rejection_count": 0
}
PostToolUse hook fields
{
"tool_name": "RunInTerminal",
"tool_input": { "command": "python -c \"print(2+2)\"", "isBackground": false },
"tool_result": "4",
"tool_succeeded": true
}
PreToolUse hook fields
{
"tool_name": "RunInTerminal",
"tool_input": { "command": "kubectl apply -f deploy.yaml" },
"tool_description": "Execute a shell command in the sandbox",
"agent_mode": "Autonomous",
"is_write_action": true,
"requires_approval": false,
"requires_browser_connection": false,
"call_id": "call_abc123"
}
The rows below are command-hook responses. Prompt hooks return only ok and
reason — the schema enforces it (PromptHookExecutor), so hookSpecificOutput
is ignored rather than rejected. Copying a row into a prompt hook silently
changes its meaning:
- Allow becomes an ordinary pass — no policy override.
- Deny (hard) becomes an ordinary soft rejection — it no longer short-circuits, so a later hook can downgrade it.
- Ask becomes an ordinary pass —
okistrue, so the tool runs without asking anyone.
Use a command hook for anything that depends on permissionDecision.
| Decision | Response format | Effect |
|---|---|---|
| No objection | {"ok": true} | Hook has no opinion on this tool. Evaluation continues to tool access policy rules and default approval checks. Does not bypass any policies. |
| Allow (policy override) | {"ok": true, "hookSpecificOutput": {"permissionDecision": "allow"}} | Marks the call approved, skipping tool access policy evaluation and default approval — including a global policy deny. It does not end the hook chain: a later hook returning deny still blocks, and a system hook that already denied short-circuited before this one ran. Only user-defined hooks (not system hooks) can trigger this. Every override is audit-logged. Restrict hook authoring to trusted administrators. |
| Deny (hard) | {"ok": false, "reason": "...", "hookSpecificOutput": {"permissionDecision": "deny"}} | Tool blocked immediately. Short-circuits the chain, so no later hook can downgrade it. Use for policy blocks that must hold. |
| Reject (soft) | {"ok": false, "reason": "..."} | Tool blocked and the reason is fed to the agent. Accumulates rather than short-circuiting — a later hook returning ask discards it and the user can approve. Use to explain a rejection, not to enforce one. |
| Ask | {"ok": true, "hookSpecificOutput": {"permissionDecision": "ask", "permissionDecisionReason": "..."}} | Execution suspends for user confirmation. |
Start hook fields
{
"start_message": "Investigate the high CPU alert on prod-web-01",
"thread_source": "Alert"
}
Start hooks are non-blocking — they cannot prevent the thread from starting. A command Start hook can inject context by returning hookSpecificOutput.additionalContext; a prompt Start hook cannot, since it returns only ok and reason. Use the sources field on the hook definition to filter by thread type (e.g., only fire for Alert or Incident threads).
Hook execution order
When multiple hooks exist (agent-level, global, system), they execute in this order:
- System global hooks — Non-bypassable safety checks (read-only guard, browser connection requirements). These are global hooks with system provenance — they cannot be configured or disabled by users.
- Agent-specific hooks — Hooks configured on the agent via portal, API, or YAML
- User global hooks — Hooks configured at the SRE Agent instance level via the global hooks API
Hooks run sequentially across all three tiers. A deny or an ask from any hook short-circuits the chain — remaining hooks are skipped, including hooks in later tiers. See Ordering and Aggregation. An allow is tracked but does not short-circuit — later hooks can still deny.
Model tiers
Prompt hooks use an AI model to evaluate agent behavior. You can select which model tier the hook uses, balancing evaluation quality against cost and latency.
| Tier | model value | Best for | Trade-off |
|---|---|---|---|
| Reasoning | ReasoningHeavy | Complex policy enforcement — multi-step validation, nuanced compliance checks | Highest quality, higher cost and latency |
| Fast Reasoning (default) | ReasoningFast | Most hooks — response validation, audit checks, safety enforcement | Good reasoning with low latency |
| General Purpose | GeneralPurpose | Simple format checks, basic compliance validation | Balanced accuracy, cost, and speed |
| Fast | SmallFast | Lightweight checks — presence validation, format verification | Lowest cost, fastest response |
| Long Context | LongContext | Hooks that process large outputs — full document analysis, extensive tool results | Handles larger input, higher cost |
Values are matched case-insensitively. Eval is also accepted, as is a
deployment name such as gpt-4.1 for direct model access. An unrecognized
value is treated as a deployment name.
Hooks default to Fast Reasoning because they run on every agent response or tool call — low latency matters. Use Reasoning only for hooks that enforce complex policies where accuracy is critical.
Limits
| Limit | Value |
|---|---|
| Script size | 64 KB maximum |
| Timeout | Must be positive. Capped at 300 seconds only by agent descriptor validation |
| Max rejections (prompt Stop hooks) | 1–25 (default: 3) |
| Supported script shebangs | #!/bin/bash, #!/usr/bin/env python3 |
| Script execution environment | Sandboxed code interpreter |
Example: Audit all tool usage
hooks:
PostToolUse:
- type: command
matcher: "*"
timeout: 30
failMode: allow
script: |
#!/usr/bin/env python3
import sys, json
context = json.load(sys.stdin)
tool_name = context.get('tool_name', 'unknown')
# The tool name is model- and agent-derived, so it stays on stderr,
# which goes to hook logs rather than the conversation. JSON-encoding it
# keeps a crafted name from forging extra log lines.
print(f"Tool used: {json.dumps(tool_name)}", file=sys.stderr)
output = {
"ok": True,
"hookSpecificOutput": {
"additionalContext": "[AUDIT] This tool call was recorded."
}
}
print(json.dumps(output))
The additionalContext field is injected as a user message into the conversation, giving the agent visibility into the audit trail. Because it reaches the model, the injected string is fixed text — copying tool_name, tool_input, or tool_result into it without validation and redaction would forward model-controlled content back into the conversation. See Hook Data Contract.
Example: Require completion marker
ok: false on Stop does not end the run — it sends the agent back to work.
maxRejections bounds that loop for prompt hooks only, so a command Stop hook
has to bound itself or it can reject forever:
hooks:
Stop:
- type: command
timeout: 30
failMode: allow
script: |
#!/usr/bin/env python3
import json, sys
context = json.load(sys.stdin)
final_output = context.get("final_output") or ""
current_turn = context.get("current_turn")
max_turns = context.get("max_turns")
# Bound the retry on the turn budget, and give up if it is unavailable.
can_retry = (
isinstance(current_turn, int)
and isinstance(max_turns, int)
and max_turns > 0
and current_turn < max_turns - 1
)
if "Task complete." in final_output or not can_retry:
print(json.dumps({"ok": True}))
else:
print(json.dumps({
"ok": False,
"reason": "Please end your response with 'Task complete.'",
}))
Best practices
- Always provide a reason when rejecting — A missing reason is replaced with a placeholder, so the rejection still takes effect but tells the agent nothing
- Use appropriate timeouts — Long-running hooks slow down agent execution
- Handle errors gracefully — Use
failMode: allowfor non-critical hooks (logging, enrichment). UsefailMode: blockfor security-critical hooks (policy enforcement, destructive command blocking) so the guardrail stays active even if the hook script fails or times out - Be specific with matchers — Overly broad PostToolUse matchers can cause performance issues
- Test hooks thoroughly — Hooks that always reject can cause loops.
maxRejectionsbounds prompt Stop hooks; command Stop hooks must bound themselves - Log to stderr — Use stderr for debugging output; stdout is parsed as the hook result
Get started
Here's what a Stop hook looks like in action — the agent initially responds with just "4", but the hook rejects because the completion marker is missing. The agent then continues and adds the marker:
| Resource | What you'll learn |
|---|---|
| Create and Manage Hooks (Portal) → | Create hooks visually in the portal UI — no API calls needed |
| Configure Agent Hooks (API) → | Set up hooks using the REST API v2 and YAML |
| Hook Data Contract → | Every payload field you can code against, and how to inspect real values |
Related capabilities
| Capability | How it relates |
|---|---|
| Run Modes → | Hooks complement run mode safety controls — modes control what, hooks control how well |
| Tool Access Policies → | Hooks evaluate before tool access policies — a hook allow overrides policy rules |
| Python Tools → | Create custom tools that hooks can audit and validate |