Skip to main content

Configure Agent Hooks (API)

Prefer the portal UI?

You can now create and manage hooks directly in the portal without using the REST API. The portal provides a visual form and code editor — no curl commands needed.

What you'll build

A custom agent with a Stop hook that forces the agent to add a completion marker to every response. You'll configure it via the REST API, then test it in the portal's playground. Time: ~15 minutes.

Agent-level vs custom-agent-level hooks

This tutorial creates hooks on a custom agent (custom-agent-level hooks). These hooks only fire when that specific custom agent runs.

To create agent-level hooks that apply to the entire agent (all threads, all custom agents), use Builder → Hooks in the portal — see Create Hooks via Portal.

LevelHow to createScope
Agent levelPortal: Builder → HooksApplies to all threads and custom agents
Custom agent levelREST API (this tutorial) or Portal: Agent Canvas → Custom agent → Manage HooksApplies only to one custom agent

Prerequisites

  • An SRE Agent in Running state
  • curl (or Postman) to call the REST API
  • Azure CLI logged in (az login) to get an access token

Before you start

This tutorial uses the REST API v2 to create hooks on a custom agent. The portal's YAML editor tab shows v1 format and doesn't display hooks configured via the API — but the hooks are still active. You can verify them in the Builder → Hooks page or the Test playground.

When to use the API vs the portal
  • Portal (Builder → Hooks): Best for agent-level hooks — visual form, no code needed
  • API (this tutorial): Best for custom-agent-level hooks, CI/CD pipelines, or programmatic management

Step 1: Find your agent's API URL

Your agent's API base URL follows this pattern:

https://{agent-name}--{hash}.{hash}.{region}.azuresre.ai

To find it:

  1. Open sre.azure.com and click on your agent
  2. In the left sidebar, click BuilderAgent Canvas
  3. Open your browser's Developer Tools (F12 or right-click → Inspect)
  4. Go to the Network tab, filter by "api", and look for requests to a URL ending in .azuresre.ai
  5. The base URL is everything before /api/...

Alternatively, check the iframe src attribute in the Elements tab — look for an <iframe> whose src starts with https://{agent-name}--.

Step 2: Get an access token

TOKEN=$(az account get-access-token \
--resource "59f0a04a-b322-4310-adc9-39ac41e9631e" \
--query accessToken -o tsv)
tip

The resource ID 59f0a04a-b322-4310-adc9-39ac41e9631e is the SRE Agent API audience. Using https://management.azure.com will return an auth error.

Step 3: Create a custom agent with a Stop hook

This creates a custom agent called my_hooked_agent with a Stop hook that checks whether the response ends with === RESPONSE COMPLETE ===. If not, it rejects and tells the agent to add the marker.

AGENT_URL="https://your-agent--xxxxxxxx.yyyyyyyy.region.azuresre.ai"

curl -X PUT "${AGENT_URL}/api/v2/extendedAgent/agents/my_hooked_agent" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d @- << 'EOF'
{
"name": "my_hooked_agent",
"properties": {
"instructions": "You are a helpful assistant. Be concise.",
"handoffDescription": "",
"handoffs": [],
"enableVanillaMode": true,
"hooks": {
"Stop": [
{
"type": "prompt",
"prompt": "Check the agent response below.\n\n$ARGUMENTS\n\nDoes it end with === RESPONSE COMPLETE ===?\nIf yes: {\"ok\": true}\nIf no: {\"ok\": false, \"reason\": \"Add === RESPONSE COMPLETE === at the end.\"}",
"timeout": 30
}
]
}
}
}
EOF

You should get HTTP 202 Accepted with the full agent config in the response body.

Here's what the same configuration looks like in v2 YAML format (for reference):

api_version: azuresre.ai/v2
kind: ExtendedAgent
metadata:
name: my_hooked_agent
spec:
instructions: |
You are a helpful assistant. Be concise.
handoffDescription: ""
enableVanillaMode: true
hooks:
Stop:
- type: prompt
prompt: |
Check the agent response below.

$ARGUMENTS

Does it end with === RESPONSE COMPLETE ===?
If yes: {"ok": true}
If no: {"ok": false, "reason": "Add === RESPONSE COMPLETE === at the end."}
timeout: 30

How the Stop hook works:

  • $ARGUMENTS gets replaced with the hook context JSON (includes the agent's final response)
  • The LLM evaluates the prompt and returns {"ok": true} or {"ok": false, "reason": "..."}
  • If rejected, the reason is injected as a user message and the agent continues working
  • After 3 rejections (the default), the agent is forced to stop

Step 4: Test it in the portal

  1. Go to your agent in the portal → BuilderAgent Canvas
  2. Select the Test playground radio button
  3. Click the Subagent/Tool dropdown, find my_hooked_agent, and click Apply
Test playground with the hooked agent selected
  1. Type "What is 2+2?" in the chat and press Send

Watch what happens:

  • The agent first responds with "4"
  • The Stop hook evaluates → rejects (no completion marker)
  • You see a "Thought process" step where the agent continues
  • The final response appears: "4 === RESPONSE COMPLETE ==="
Stop hook result — agent adds === RESPONSE COMPLETE === after initial rejection

The hook worked — it forced the agent to add the marker before stopping.


Step 5: Add a PostToolUse hook for auditing

Now add a PostToolUse hook that logs every tool the agent uses. Update the same agent by sending a new PUT with both hooks:

curl -X PUT "${AGENT_URL}/api/v2/extendedAgent/agents/my_hooked_agent" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d @- << 'EOF'
{
"name": "my_hooked_agent",
"properties": {
"instructions": "You are a helpful assistant. Be concise.",
"handoffDescription": "",
"handoffs": [],
"enableVanillaMode": true,
"hooks": {
"Stop": [
{
"type": "prompt",
"prompt": "Check the agent response below.\n\n$ARGUMENTS\n\nDoes it end with === RESPONSE COMPLETE ===?\nIf yes: {\"ok\": true}\nIf no: {\"ok\": false, \"reason\": \"Add === RESPONSE COMPLETE === at the end.\"}",
"timeout": 30
}
],
"PostToolUse": [
{
"type": "command",
"matcher": "*",
"timeout": 30,
"failMode": "allow",
"script": "#!/usr/bin/env python3\nimport sys, json\ncontext = json.load(sys.stdin)\ntool = context.get('tool_name', 'unknown')\nprint(json.dumps({'decision': 'allow', 'hookSpecificOutput': {'additionalContext': f'[AUDIT] {tool} executed.'}}))"
}
]
}
}
}
EOF

matcher: "*" means this hook runs for every tool call. The script logs the tool name and injects an [AUDIT] message into the conversation.

To test: ask the agent a question that triggers a tool (e.g., "Run echo hello").


Step 6: Block dangerous commands

Add a second PostToolUse hook that blocks rm -rf, sudo, and chmod 777:

PostToolUse:
# Audit hook (runs for all tools)
- type: command
matcher: "*"
timeout: 30
failMode: allow
script: |
#!/usr/bin/env python3
import sys, json
context = json.load(sys.stdin)
tool = context.get('tool_name', 'unknown')
print(json.dumps({"decision": "allow",
"hookSpecificOutput": {"additionalContext": f"[AUDIT] {tool} executed."}}))

# Policy hook (only for shell tools)
- type: command
matcher: "Bash|ExecuteShellCommand"
timeout: 30
failMode: block
script: |
#!/usr/bin/env python3
import sys, json, re
context = json.load(sys.stdin)
command = context.get('tool_input', {}).get('command', '')
for pattern in [r'\brm\s+-rf\b', r'\bsudo\b', r'\bchmod\s+777\b']:
if re.search(pattern, command):
print(json.dumps({"decision": "block", "reason": f"Blocked: {pattern}"}))
sys.exit(0)
print(json.dumps({"decision": "allow"}))

Key differences from the audit hook:

  • matcher: "Bash|ExecuteShellCommand" — only runs for shell tools (pattern is anchored as ^(Bash|ExecuteShellCommand)$)
  • failMode: block — if the script itself crashes, the tool result is blocked (strict mode)
  • Returns "block" with a reason when a dangerous pattern is found

Hook response formats

Prompt hooks return simple JSON:

{"ok": true}
{"ok": false, "reason": "Please fix X."}

Command hooks return expanded JSON:

{"decision": "allow"}
{"decision": "block", "reason": "Dangerous command."}
{"decision": "allow", "hookSpecificOutput": {"additionalContext": "Audit note."}}

Command hooks can also use exit codes instead of JSON:

Exit codeBehavior
0 with no outputAllow
0 with JSONParse the JSON
2Block — stderr becomes the reason
OtherFalls back to failMode
caution

A rejection without a reason is treated as approval. Always include reason when rejecting.


What you learned

  • Custom-agent-level hooks are configured via the REST API v2 and apply only to that custom agent
  • Agent-level hooks are created in Builder → Hooks and apply across the entire agent (see the portal tutorial)
  • Stop hooks reject incomplete responses and force the agent to continue
  • PostToolUse hooks audit tool usage and can block dangerous commands
  • Prompt hooks use LLM evaluation; command hooks use scripts with exit codes
  • Always include a reason when rejecting — without it, rejection is treated as approval

Troubleshooting

IssueSolution
Hooks not visible in portal YAML tabExpected — the YAML tab shows v1 only. Custom-agent-level hooks created via API are active and visible in Builder → Hooks or the playground
Unsupported kind: ExtendedAgentUse v2 endpoint: PUT /api/v2/extendedAgent/agents/{name}
Handoffs cannot be nullAdd "handoffs": [] to the JSON payload
Auth error (audience validation)Use resource 59f0a04a-b322-4310-adc9-39ac41e9631e for the token
Hook has no effectInclude a reason field when rejecting — without it, rejection = approval
Agent loops foreverLower maxRejections (default: 3, range: 1–25)
ResourceWhat you'll learn
Agent Hooks capability overviewFull hook reference, context schema, and limits
Create and Manage Hooks (Portal)Create hooks visually in the portal UI
Run ModesHow hooks complement safety controls
Python ToolsBuild custom tools your hooks can monitor
Was this page helpful?