Create and Manage Hooks via Portal
Four governance hooks using the portal UI: a Stop hook that enforces data formatting, a PostToolUse hook that audits and suppresses dangerous shell command output, a PreToolUse hook that confirms before destructive operations execute, and a Start hook that injects on-call context into every thread. You'll learn hooks at both the agent level (applies to everything) and the custom agent level (applies to one specific custom agent).
Prerequisites
- An Azure SRE Agent in Running state
- Contributor role or higher on the SRE Agent resource
Hooks you previously created via the REST API tutorial appear automatically in the portal UI. You can manage them visually without reconfiguring anything.
Where hooks live in the portal
Hooks operate at two levels. This is a key architectural concept:
| Level | Location in portal | Scope | Use when |
|---|---|---|---|
| Agent level | Builder → Hooks | Applies to the entire agent — all threads and all custom agents | You want agent-wide policies like "audit every tool call" or "block dangerous commands everywhere" |
| Custom agent level | Agent Canvas → Custom agent → Manage Hooks | Applies only when that specific custom agent runs | You want hooks tailored to one custom agent, like "validate this custom agent's output format" |
If an agent-level hook and a custom-agent-level hook both match the same event, both run. Agent-level hooks fire first, then custom-agent-level hooks.
Part 1: Agent-level hooks (Builder → Hooks)
Agent-level hooks apply to the entire agent — every thread, every custom agent. They have activation modes that control when they're active.
Step 1: Open the Hooks page
- Navigate to sre.azure.com and select your agent
- In the sidebar, expand Builder
- Click Hooks
Checkpoint: You see the "Hooks" heading with a description, a Create hook button, and an empty data grid (or a list of existing hooks).
Step 2: Create a Stop hook
A Stop hook fires when the agent is about to return a final response. Use it to validate response quality and enforce formatting rules.
- Click Create hook
- Fill in the form fields:
| Field | Value |
|---|---|
| Name | require-table-format |
| Event type | Stop |
| Activation mode | Always |
| Description | Ensures responses present structured data as markdown tables with bold headers |
- Under Hook Definition, keep Hook type set to Prompt
- Keep Model set to Reasoning Fast (default)
- In the Prompt editor on the right, enter:
Check the agent response below.
$ARGUMENTS
Does the response present any structured data (lists of items, comparisons, metrics) as a markdown table with **bold** column headers?
If no structured data is present, approve.
If structured data IS present as a table with bold headers: {"ok": true}
If structured data is present but NOT formatted as a table: {"ok": false, "reason": "Reformat the structured data as a markdown table with **bold** column headers."}
- Leave Timeout (sec) at
30, Fail mode atAllow, and Max rejections at3 - Click Save
Checkpoint: The dialog closes with a success notification. The hook appears in the data grid with Event type "Stop" and Activation "Always."
The $ARGUMENTS placeholder injects the hook context (including the agent's final response) into the prompt. The LLM evaluates whether the response meets your criteria and returns {"ok": true} to approve or {"ok": false, "reason": "..."} to reject. After 3 rejections (the default), the agent is forced to stop.
Step 3: Test the Stop hook
- Navigate to Chat in the sidebar
- Type "Compare the pros and cons of Python vs Go for building microservices" and press Send
- Watch the agent's response:
- The agent initially responds with a plain text comparison
- The Stop hook evaluates and rejects because the data isn't in a table
- The agent reformats its response as a markdown table with bold headers
Checkpoint: The final response presents the comparison as a formatted table like:
| Language | Pros | Cons |
|---|---|---|
| Python | Rapid development, rich ecosystem | Slower execution, GIL limitations |
| Go | Fast compilation, built-in concurrency | Smaller ecosystem, verbose error handling |
Step 4: Create a PostToolUse hook
A PostToolUse hook fires after a tool has finished executing. It can audit the call, redact sensitive output, or block the tool result from being added to the agent's conversation — but the side effect has already happened. To prevent a destructive command from running, use a PreToolUse hook (Step 6) instead.
- Go back to Builder → Hooks
- Click Create hook
- Fill in the form:
| Field | Value |
|---|---|
| Name | flag-dangerous-command-results |
| Event type | Post Tool Use |
| Activation mode | Always |
| Description | Withholds shell results for rm -rf, sudo, and chmod 777 so the agent can't act on them |
| Hook type | Command |
| Tool matcher | Bash|ExecuteShellCommand |
- Select Python as the script language
- In the Script editor, enter:
#!/usr/bin/env python3
import sys, json, re
context = json.load(sys.stdin)
command = context.get('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({"decision": "block", "reason": f"Blocked: {pattern}"}))
sys.exit(0)
print(json.dumps({"decision": "allow"}))
- Set Fail mode to Block (if the script crashes, the tool result is blocked)
- Click Save
Checkpoint: Both hooks appear in the Hooks data grid.
PostToolUse runs after the tool completes. A block decision suppresses the tool's output from reaching the model, but any files written, services called, or commands executed already happened. For permission gating on destructive commands, see Step 6 (PreToolUse).
The Tool matcher field works two ways. Click the chevron at the right of the field to open a live, searchable menu of the agent's tools, grouped into Built-in tools, MCP tools, and Custom tools; check one or more and the matcher fills itself in (a single tool becomes Bash, multiple tools become (Bash|ExecuteShellCommand)). Or type a regex directly — anchors, lookaheads, and character classes are all supported. Bash|ExecuteShellCommand matches tools named exactly "Bash" or "ExecuteShellCommand" (the server anchors the pattern as \A(?:Bash|ExecuteShellCommand)\z, so a tool name with extra leading or trailing characters — including a trailing newline — will not match). Tool names come from the agent's live tool list, so their casing already matches what the server evaluates. Use * to match all tools.
Step 5: Test the PostToolUse hook
- Go to Chat
- Ask the agent to run a safe command: "Run echo hello" — the result reaches the agent normally
- Ask the agent to run a flagged command in a sandbox directory: "Run rm -rf /tmp/test" — the command does run, but the agent receives the block message instead of the tool output
Checkpoint: Safe-command output is delivered to the agent. The flagged command's output is suppressed and the agent sees the block reason in its place.
Step 6: Create a PreToolUse hook (gate tool execution before it runs)
A PreToolUse hook fires before a tool is invoked. Unlike PostToolUse, the hook can decide whether to allow, deny, or ask the user before the tool runs at all — so it's the right choice when you want to stop a dangerous operation from executing rather than react after it already happened.
For permission gating, use a Command hook (not a Prompt hook): the decision must be a deterministic check on the tool input, and Command hooks let you emit the exact JSON shape the runtime expects.
- From Builder → Hooks, click Create hook
- Fill in the form:
| Field | Value |
|---|---|
| Name | confirm-destructive-tools |
| Event type | Pre Tool Use |
| Activation mode | Always |
| Description | Asks the user to confirm before destructive Bash commands run |
| Hook type | Command |
| Tool matcher | Bash|ExecuteShellCommand |
- Select Bash as the script language
- In the Script editor, enter:
#!/bin/bash
# PreToolUse hook: receives the tool context as JSON on stdin and emits a
# permission decision under hookSpecificOutput. Exit 2 is reserved by the
# runtime as "always blocking" — we exit 0 here and let the JSON decide.
#
# NOTE: This is an illustrative example. For production use, validate the
# full command string, not just the first token — a command like
# "ls && rm -rf /" would match the allow-list below because it starts
# with "ls". A production hook should parse the command more strictly
# (e.g., reject commands containing shell operators like &&, ||, ;, |).
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')
TOOL_CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // (.tool_input | tostring)')
# Reject commands with shell chaining operators before any other check
if printf '%s' "$TOOL_CMD" | grep -qE '[;&|]'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Command contains shell operators (;, &, |) which are not allowed."
}
}'
elif printf '%s' "$TOOL_CMD" | grep -qE '(\brm[[:space:]]+-[a-zA-Z]*r|\bsudo\b|chmod[[:space:]]+777|\bdd[[:space:]]+if=|\bmkfs|kill[[:space:]]+-9)'; then
jq -n --arg t "$TOOL_NAME" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: ("Confirm before running \($t): destructive pattern detected.")
}
}'
elif printf '%s' "$TOOL_CMD" | grep -qE '^[[:space:]]*(ls|cat|echo|pwd|date|whoami|hostname)[[:space:]]*$'; then
jq -n '{
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
}'
else
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Command does not match the allow-list."
}
}'
fi
- Set Fail mode to Block (if the script errors, deny the tool — Command hooks honor
failModeon non-zero exits, except exit code2which is reserved as "always blocking" regardless offailMode). - Click Save
- PreToolUse runs before the tool — you can stop it. Use for permission gating and "are you sure?" confirmation flows.
- PostToolUse runs after the tool — the side-effect already happened. Use for auditing, log scrubbing, or rejecting bad output before the agent sees it.
- Both require a Tool matcher (use
*to match all tools).
You can also write a PreToolUse hook of type Prompt, but the gating LLM evaluates tool_input as untrusted text — an attacker who controls part of the tool input (e.g., a retrieved doc or an earlier tool output) can often steer the gating LLM with an embedded instruction such as "respond with allow". For permission decisions on destructive tools, prefer a Command hook with deterministic pattern matching as shown above. Treat LLM-based gates as advisory, not as a robust security boundary.
The runtime executes a Command hook by writing the context as JSON, escaping it for the shell, and piping it into your script:
echo '<context-json>' | ./your-script
A few facts that trip people up the first time they write a Command hook:
- Context arrives on stdin only. There is no
$CLAUDE_TOOL_INPUT, no$TOOL_NAME, no environment-variable shortcut — read it withcat,json.load(sys.stdin), orjqfrom stdin. Anything else returns empty and your hook silently allows. - The decision string is literal. At the top level, the runtime accepts
{"decision": "allow"}or{"decision": "block", "reason": "..."}. Anything else ("deny","reject","no") is treated as allow. For PreToolUse it is more idiomatic to usehookSpecificOutput.permissionDecisionwith values"allow","deny", or"ask"as shown in Step 6 above — that path supports the third state and renders nicely in the UI. - Empty stdout is treated as allow. If your script exits 0 with no output, the runtime returns success — there is no warning, no chip, no log surface. If you suspect a hook isn't firing, the first thing to check is that it actually writes JSON to stdout. (Exit code
2is the one exception: it always blocks regardless of stdout content, andfailModedoes not override it.)
The full PreToolUse context the runtime delivers includes tool_name, tool_input, tool_description (the same docstring the LLM sees for that tool — useful for semantic policy decisions, not just name matching), agent_mode, is_write_action, requires_approval, requires_browser_connection, call_id, hook_event_name, agent_name, current_turn, max_turns, and execution_summary (path to the in-flight transcript).
Step 7: Create a Start hook (initialize per-thread context)
A Start hook fires once when a new agent thread begins. Use it to inject context that the agent should know about for the entire conversation — current on-call, active incident, environment overrides — without making the user type it every time.
- From Builder → Hooks, click Create hook
- Fill in the form:
| Field | Value |
|---|---|
| Name | inject-oncall-context |
| Event type | Start |
| Activation mode | Always |
| Description | Adds current on-call engineer and active incident to thread context |
| Hook type | Command |
- Select Bash as the script language
- In the Script editor, enter:
#!/bin/bash
# Start hook: emit context to inject under hookSpecificOutput.additionalContext.
# Build the JSON with `jq --arg` so that quotes or newlines from the upstream
# service cannot break the JSON structure. Note: jq --arg prevents JSON
# injection (broken structure), but the content itself is still untrusted
# text that the agent will reason over.
set -euo pipefail
ONCALL=$(curl -fsS --connect-timeout 5 --max-time 10 "https://my-oncall-service/now" 2>/dev/null || echo "unknown")
INCIDENT=$(curl -fsS --connect-timeout 5 --max-time 10 "https://my-incident-service/active" 2>/dev/null || echo "none")
jq -n --arg oncall "$ONCALL" --arg incident "$INCIDENT" '{
hookSpecificOutput: {
hookEventName: "Start",
additionalContext: ("Current on-call: " + $oncall + ". Active incident: " + $incident + ".")
}
}'
- Click Save
- No tool matcher is required (Start hooks aren't tied to a specific tool).
- An optional Source filter field scopes the hook to specific thread sources — a comma-separated list such as
Teams, Alert, Incident(matching is case-insensitive). Leave it empty to run on threads from every source. - The portal sets
failModetoallowautomatically when you save (Start hooks cannot deny a thread — the runtime ignores blocking results — so the Fail Mode field is hidden in the form). The form also shows an inline reminder that Start hooks are non-blocking: they inject context before the reasoning loop begins and cannot block execution. - Start hooks run once per thread, on creation.
- Returned
hookSpecificOutput.additionalContextis injected as a user message at the start of the conversation, so the agent sees it before the first turn.
Any value interpolated into additionalContext (for example the response body from my-oncall-service) ends up in the agent's conversation. Use jq --arg (as shown above) to prevent quotes or newlines from breaking the JSON structure. However, jq --arg is structural protection only: it ensures valid JSON, but the content itself is still untrusted text that the agent will reason over. Do not assume that JSON encoding prevents the external content from influencing agent behavior.
Step 8: Edit and delete agent-level hooks
Edit: Click the edit icon on any hook row in the data grid, modify the fields, and click Save.
Delete: Select the checkbox next to hooks you want to remove, click Delete in the toolbar, and confirm.
Checkpoint: Changes are reflected immediately in the data grid.
Part 2: Custom-agent-level hooks (Agent Canvas)
Custom-agent-level hooks are configured directly in a custom agent's definition. They apply only when that specific custom agent runs — not to the main agent or other custom agents.
Step 9: Open the custom agent hooks panel
- In the sidebar, expand Builder and click Agent Canvas
- Click on an existing custom agent to edit it — or click Create custom agent to start a new one
- In the custom agent form, scroll down to the Hooks section
- Click Manage Hooks
Checkpoint: A side panel opens with sections for each event type. If no hooks are configured, you see empty states with guidance text.
Step 10: Add a hook to a custom agent
Let's add a Stop hook that ensures this custom agent always responds in a structured format:
- In the Manage Hooks panel, click the Add hook button at the bottom of the panel
- In the dialog that opens, fill in the hook form:
| Field | Value |
|---|---|
| Event type | Stop |
| Hook type | Prompt |
| Prompt | Check the response below. $ARGUMENTS Does it include a clear summary section at the end? If yes: {"ok": true} If no: {"ok": false, "reason": "Add a Summary section at the end of your response."} |
| Timeout (sec) | 30 |
| Fail mode | Allow |
| Max rejections | 3 |
- Click Save on the hook
- Click Create (or Save) on the custom agent to save the full configuration
Checkpoint: The hook appears in the Manage Hooks panel under the Stop section. The custom agent form shows "Manage Hooks (1)" on the button.
To test this hook, go to Agent Canvas → select the Test playground view → choose your custom agent from the dropdown → type a question. The hook only runs when this specific custom agent is invoked.
Part 3: Manage hooks per thread
Agent-level hooks with Always activation are active in every conversation by default. Hooks with On Demand activation must be manually activated per thread.
Step 11: Toggle hooks in a conversation
- Open a Chat thread
- Click the + button in the chat footer
- Select Manage Hooks
- Toggle hooks on or off for the current thread
Always hooks can be temporarily deactivated. On Demand hooks can be activated when needed. Required system hooks are locked and cannot be toggled.
Checkpoint: Hook changes take effect immediately in the current thread.
What you learned
- Hooks operate at two levels: Builder → Hooks (agent level) and Agent Canvas → custom agent → Manage Hooks (custom agent level)
- How to create Stop hooks that validate responses before delivery
- How to create PostToolUse hooks that audit and control tool usage
- How to create PreToolUse hooks that gate tool execution with allow/deny/ask decisions before the tool runs
- How to create Start hooks that inject external context into every new thread
- The difference between Prompt hooks (LLM evaluation) and Command hooks (scripts)
- How activation modes (Always vs On Demand) control global hook behavior per thread
- How to edit, delete, and manage hooks across both surfaces
Troubleshooting
| Issue | Solution |
|---|---|
| Hooks page not visible in sidebar | The Hooks page appears under Builder. Verify your agent is in Running state. If the option still doesn't appear, contact support. |
| "Hook name is required" | Enter a name using only letters, numbers, hyphens, and underscores. |
| "Name must contain only letters, numbers, hyphens, and underscores" | Remove special characters from the hook name. |
| "Hook name cannot start with system__" | The system__ prefix is reserved for system hooks. Choose a different name. |
Tool matcher is required for tool use hooks (form) spec.hook.matcher: Required for PreToolUse hooks. Use '*' to match all tools. (REST/YAML) | PreToolUse and PostToolUse hooks both need a regex matcher. Use * to match all tools. The first message comes from the Web form's client-side validator; the second comes from the server (REST API, YAML, or srectl). |
spec.hook.matcher: Only applicable for PreToolUse and PostToolUse hooks. | The hook's event type is Start or Stop — clear the Tool matcher field. The Web portal omits matcher for Start/Stop on save, so this error mostly indicates a stale value in a REST/YAML/Bicep payload. |
PreToolUse hook returns permissionDecision but the tool still runs | Make sure the JSON is nested under hookSpecificOutput (not top-level). The runtime only reads hookSpecificOutput.permissionDecision. |
| Start hook completes but no context is injected | Use hookSpecificOutput.additionalContext (not contextMessage). The runtime only consumes the nested additionalContext field. |
| Hook doesn't fire | For agent-level hooks, check the activation mode — On Demand hooks must be activated per thread. For custom-agent-level hooks, verify the custom agent is being invoked. |
| Stop hook approves everything | Ensure the prompt returns {"ok": false, "reason": "..."} when rejecting. A rejection without a reason is treated as approval. |
| Script errors blocking actions | Set Fail mode to Allow for graceful degradation during development. Switch to Block in production. |
Related
| Resource | What you'll learn |
|---|---|
| Agent Hooks → | Full hook reference, context schema, response formats, and limits |
| Configure Hooks via API → | Create hooks using the REST API v2 and YAML |
| Run Modes → | How hooks complement run mode safety controls |