Skip to main content

Create and Manage Hooks via Portal

What you'll build

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
Already using the REST API?

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:

LevelLocation in portalScopeUse when
Agent levelBuilder → HooksApplies to the entire agent — all threads and all custom agentsYou want agent-wide policies like "audit every tool call" or "block dangerous commands everywhere"
Custom agent levelAgent Canvas → Custom agent → Manage HooksApplies only when that specific custom agent runsYou want hooks tailored to one custom agent, like "validate this custom agent's output format"
Both levels can coexist

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

  1. Navigate to sre.azure.com and select your agent
  2. In the sidebar, expand Builder
  3. 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.

  1. Click Create hook
  2. Fill in the form fields:
FieldValue
Namerequire-table-format
Event typeStop
Activation modeAlways
DescriptionEnsures responses present structured data as markdown tables with bold headers
  1. Under Hook Definition, keep Hook type set to Prompt
  2. Keep Model set to Reasoning Fast (default)
  3. 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."}
  1. Leave Timeout (sec) at 30, Fail mode at Allow, and Max rejections at 3
  2. Click Save

Checkpoint: The dialog closes with a success notification. The hook appears in the data grid with Event type "Stop" and Activation "Always."

How the Stop hook works

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

  1. Navigate to Chat in the sidebar
  2. Type "Compare the pros and cons of Python vs Go for building microservices" and press Send
  3. 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:

LanguageProsCons
PythonRapid development, rich ecosystemSlower execution, GIL limitations
GoFast compilation, built-in concurrencySmaller 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.

  1. Go back to Builder → Hooks
  2. Click Create hook
  3. Fill in the form:
FieldValue
Nameflag-dangerous-command-results
Event typePost Tool Use
Activation modeAlways
DescriptionWithholds shell results for rm -rf, sudo, and chmod 777 so the agent can't act on them
Hook typeCommand
Tool matcherBash|ExecuteShellCommand
  1. Select Python as the script language
  2. 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"}))
  1. Set Fail mode to Block (if the script crashes, the tool result is blocked)
  2. Click Save

Checkpoint: Both hooks appear in the Hooks data grid.

PostToolUse cannot undo a side effect

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).

Tool matcher — pick from the menu or write a regex

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

  1. Go to Chat
  2. Ask the agent to run a safe command: "Run echo hello" — the result reaches the agent normally
  3. 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.

  1. From Builder → Hooks, click Create hook
  2. Fill in the form:
FieldValue
Nameconfirm-destructive-tools
Event typePre Tool Use
Activation modeAlways
DescriptionAsks the user to confirm before destructive Bash commands run
Hook typeCommand
Tool matcherBash|ExecuteShellCommand
  1. Select Bash as the script language
  2. 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
  1. Set Fail mode to Block (if the script errors, deny the tool — Command hooks honor failMode on non-zero exits, except exit code 2 which is reserved as "always blocking" regardless of failMode).
  2. Click Save
PreToolUse vs PostToolUse
  • 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).
LLM-based gates are advisory, not a security boundary

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.

Command hook runtime contract — three gotchas worth knowing

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:

  1. Context arrives on stdin only. There is no $CLAUDE_TOOL_INPUT, no $TOOL_NAME, no environment-variable shortcut — read it with cat, json.load(sys.stdin), or jq from stdin. Anything else returns empty and your hook silently allows.
  2. 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 use hookSpecificOutput.permissionDecision with values "allow", "deny", or "ask" as shown in Step 6 above — that path supports the third state and renders nicely in the UI.
  3. 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 2 is the one exception: it always blocks regardless of stdout content, and failMode does 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.

  1. From Builder → Hooks, click Create hook
  2. Fill in the form:
FieldValue
Nameinject-oncall-context
Event typeStart
Activation modeAlways
DescriptionAdds current on-call engineer and active incident to thread context
Hook typeCommand
  1. Select Bash as the script language
  2. 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 + ".")
}
}'
  1. Click Save
Start hook notes
  • 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 failMode to allow automatically 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.additionalContext is injected as a user message at the start of the conversation, so the agent sees it before the first turn.
Treat external content as untrusted

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

  1. In the sidebar, expand Builder and click Agent Canvas
  2. Click on an existing custom agent to edit it — or click Create custom agent to start a new one
  3. In the custom agent form, scroll down to the Hooks section
  4. 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:

  1. In the Manage Hooks panel, click the Add hook button at the bottom of the panel
  2. In the dialog that opens, fill in the hook form:
FieldValue
Event typeStop
Hook typePrompt
PromptCheck 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 modeAllow
Max rejections3
  1. Click Save on the hook
  2. 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.

Testing custom-agent-level hooks

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

  1. Open a Chat thread
  2. Click the + button in the chat footer
  3. Select Manage Hooks
  4. 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

IssueSolution
Hooks page not visible in sidebarThe 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 runsMake sure the JSON is nested under hookSpecificOutput (not top-level). The runtime only reads hookSpecificOutput.permissionDecision.
Start hook completes but no context is injectedUse hookSpecificOutput.additionalContext (not contextMessage). The runtime only consumes the nested additionalContext field.
Hook doesn't fireFor 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 everythingEnsure the prompt returns {"ok": false, "reason": "..."} when rejecting. A rejection without a reason is treated as approval.
Script errors blocking actionsSet Fail mode to Allow for graceful degradation during development. Switch to Block in production.
ResourceWhat 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
Was this page helpful?