Key Takeaway
Azure SRE Agent has no single approval switch. It stacks hooks, allow, ask and deny tool policies, connector approvals and run modes, in that priority order. Only a global deny always holds: Ask is auto-approved in Autonomous mode, and any user can add a thread-level allow. Put never-do commands in global deny and run production triggers in Review.
Before I would let an on-call agent near a production subscription, I wanted to know exactly which of its actions a human sees first. So I read Microsoft's Learn pages for Azure SRE Agent and the microsoft/sre-agent repository end to end, including the lab recipes that ship real policy and hook files.
I did not deploy an agent for this post. Every rule below is quoted or paraphrased from the pages I read, and where I extrapolate from a documented rule I say so. The finding is that the defaults are sensible and the sharp edges are specific: they sit in the gaps between four controls, not inside any one of them.
Review mode gates less than its name suggests
Run modes decide whether the agent asks before acting. In Review it proposes and you approve or deny; in Autonomous it executes immediately and reports back. The agent as a whole defaults to Review, set when you create it, but the mode that actually applies is chosen per response plan and per scheduled task, and the docs list the default for both of those as Autonomous. Reading the trigger list matters more than reading the agent setting.
The catch is scope. Review shows Approve and Deny buttons only for Azure infrastructure operations, meaning Azure CLI commands, Azure Resource Manager operations and similar write actions. Sending email, posting to Teams or querying an external data source proceeds on the agent's own reasoning and your response plan instructions. Only SRE Agent Administrators can approve. So Review is an approval workflow for Azure writes, not a general change gate, and the docs themselves send you to hooks and tool access policies for everything else.
Four controls, one priority order
The tool access policies page spells out who wins when controls disagree. From highest to lowest priority:
| Priority | Control | What it can decide | Who sets it |
|---|---|---|---|
| Highest | Hooks | Allow, block or ask per tool call. A hook allow skips everything below it | SRE Agent Administrators only |
| Second | Tool access policies | Allow, ask or deny by tool-name glob | Global: admin. Custom agent: admin or author. Thread: any user |
| Third | Default approval | Connector Ask and RequiresApproval tools pause for a person | Whoever configures the connector |
| Lowest | Run modes | Review pauses, Autonomous auto-approves | Per response plan and scheduled task, plus an agent-wide default |
Permissions are a separate axis: the docs say run modes control the approval workflow while permissions control resource access, and the agent needs both to act. One line in the table deserves a second read. A user-defined hook that returns allow overrides policy rules, including a global deny. System hooks cannot do this, every override is audit-logged, and only Administrators can create hooks. So the person who can author a hook holds more power than the person who writes the deny list, and hook authorship should be reviewed like an IAM change.
Allow, ask and deny: scopes and evaluation order
Policies exist at three scopes. Global is set by an admin and may allow, ask or deny. Custom agent, set by an admin or author, and thread, set by any user, may only allow. Only the global scope can deny, so a narrower scope widens access inside global limits but can never weaken a global deny. When the agent calls a tool the order is: deny, then allow, then ask, then default behaviour. An allow runs immediately even in Review mode, and if a tool matches both allow and ask, allow wins.
# Global policy, the only scope that can deny. The first three patterns
# are the documented example; the last two are my additions in the same
# toolGlob(argGlob) syntax and I have not run them against a live agent.
curl -X PUT "https://<agent>/api/v2/agent/settings/global" \
-H "Content-Type: application/json" \
-d '{
"permissions": {
"allow": [
"RunAzCliReadCommands",
"RunKubectlReadCommand(kubectl get *)"
],
"ask": [
"RunKubectlWriteCommand(kubectl apply *)"
],
"deny": [
"bash(az * delete *)",
"RunKubectlWriteCommand(kubectl delete *)",
"RunAzCliWriteCommands(az role assignment *)"
]
}
}'The onboarding lab in the repository ships exactly this shape in tool-permissions.json: read tools allowed by name, one ask on RunAzCliWriteCommands, and denies on kubectl writes, terminal access and every file-writing tool.
// labs/onboardinglab/agent-recipe/tool-permissions.json (microsoft/sre-agent)
// Reads by name, exactly one ask, everything that mutates is denied.
{
"allow": [
"GetAzCliHelp", "RunAzCliReadCommands", "QueryAppInsightsUsingAppId",
"ReadFile", "ListDir", "FileSearch", "GrepSearch", "read_skill_file",
"system-mcp-monitor/*"
],
"ask": [ "RunAzCliWriteCommands" ],
"deny": [
"RunKubectlWriteCommand", "RunInTerminal", "Terminal", "CreateFile",
"CreateDirectory", "SaveFileToBlob", "ReplaceStringInFile",
"MultiReplaceStringInFile"
]
}Treat that file as one design, not the Microsoft default. The dynatrace-servicenow recipe in the same repository allows RunInTerminal and ExecutePythonCode and denies FetchWebpage and SearchWebpage. Which tools an agent may touch is your decision per agent, and the repository shows both extremes.
Argument patterns and the Python gap
For command tools you can write toolGlob(argGlob), for example RunKubectlReadCommand(kubectl get *), and the doc lists ten runtime tool names that accept arguments. The aliases bash, shell and execute_bash expand to every command-string tool, which covers Azure CLI, kubectl and shell commands, but not Python or PDF tools. For Python you must name ExecutePythonCode directly.
// Wrong: this looks like "no deletes", but the alias covers command tools only.
// An agent that is allowed ExecutePythonCode and holds write RBAC can still
// call a delete through an SDK, and no rule above ever sees it.
"deny": [ "bash(az * delete *)" ]
// Right: name the Python tool too, or do not give the agent write RBAC at all.
"deny": [
"bash(az * delete *)",
"RunKubectlWriteCommand(kubectl delete *)",
"ExecutePythonCode"
]That alias rule has a consequence the page does not state, so treat this as my inference. A deny written as bash(az * delete *) never sees a delete issued from a Python script, so an agent that is allowed ExecutePythonCode and holds a write role can route around it. Either deny the Python tool on agents with write access or keep their identity read-only. Each scope holds at most 1,000 patterns, which is plenty; the risk is coverage, not count.
Hooks: Stop and PostToolUse, and what neither can do
The hooks page lists two events. Stop fires when the agent is about to return its final response and can reject it, forcing the agent to continue. PostToolUse fires after a tool finishes successfully and can audit, block the result or inject context. A hook is either a prompt, where an LLM returns a JSON verdict, or a command, where a bash or Python script of up to 64 KB runs in a sandbox with a timeout of 1 to 300 seconds. Agent-level and custom-agent hooks both run, agent level first.
The repository's lab hooks use a Stop prompt hook as an approval gate. The VM remediation hook lists restarting or resizing a VM, az vm run-command, NSG rule changes and tag changes, tells the reviewer to reject and ask for a yes, and runs always. The deployment compliance hook does the same for Container App revision changes, runs on-demand per thread, uses the ReasoningFast model with a 30 second timeout, and fails closed.
# labs/deployment-compliance/hooks/deployment-compliance-approval.yaml (trimmed)
name: deployment-compliance-approval
event_type: Stop
activation_mode: on-demand # the sibling VM hook uses "always"
hook:
type: prompt
prompt: |
You are a deployment safety reviewer. Check the agent's proposed response
for any of these actions:
1. Reverting a Container App to a previous revision
2. Activating or deactivating a Container App revision
3. Changing Container App ingress traffic weights
4. Running az containerapp update / revision activate / revision deactivate
/ ingress traffic set
If the agent is about to perform ANY of these actions: REJECT, explain
what it wants to do, ask the user to reply "yes" or "no".
$ARGUMENTS
Respond with a JSON object: {"ok": true, "reason": "..."} or
{"ok": false, "reason": "Deployment revert requires approval. ..."}
model: ReasoningFast
timeout: 30
fail_mode: Block # fail closed: a hook error blocks, not allows
max_rejections: 3Read the design for what it is. A Stop hook judges the agent's proposed final response, so the gate is conversational: the agent says what it wants to do, the hook rejects it, and a person replies yes. That is a decent human-in-the-loop for chat, enforced by an LLM's judgement rather than by a deterministic rule. The docs themselves note that command hooks are the better fit for deterministic checks.
Timing is the second limit. PostToolUse fires after a tool has finished successfully, so by then a write has already happened; it audits and informs, it does not prevent. The tool access policies page does describe a hook deny blocking immediately, but the hooks page lists only the two events above, and I could not reconcile how a pre-execution hook is configured from the pages I read. For stopping a write before it runs, I would rely on deny and ask policies and leave hooks to audit and completion checks.
When you write a command hook, exit code 2 always blocks and puts stderr in front of the agent as the reason, and any other non-zero code falls back to failMode. Log to stderr, print only the JSON verdict to stdout, and always include a reason when you reject.
Where the guardrails fail
The same policy produces different behaviour depending on the run mode of the trigger that fired. This is the documented outcome for each rule match:
| Rule that matched | In Review mode | In Autonomous mode |
|---|---|---|
| Global deny | Blocked | Blocked |
| Allow at any scope | Runs immediately | Runs immediately |
| Global ask | User approves | Auto-approved |
| No policy match, connector tool set to Ask | User approves | Auto-approved |
| No policy match, nothing else applies | Executes | Executes |
The managed connectors page says it plainly: in Autonomous mode, including scheduled tasks, HTTP triggers and incident auto-response, every tool set to Ask executes without human approval, and the setting completely bypasses the approval guardrail. An Ask rule is a Review-mode control. Anything you would never let run unattended belongs in deny.
Four more traps come straight from the pages I read:
- Ask is a control against the agent, not against a user. A thread-level allow can be added by any user and beats an ask rule, so anyone in a conversation can pre-approve a tool they trust. Deny cannot be widened below global scope.
- A Stop hook that rejects without a reason is treated as approval and the agent simply stops. A rejection with no reason field is a silent pass.
- failMode defaults to allow, so a hook that times out or errors fails open. The best-practice list says to use allow unless strict enforcement is required, which means an approval hook is exactly the case where you must set block. The deployment lab hook sets fail_mode Block; the VM lab hook file sets none.
- A hook that always rejects loops. maxRejections caps this, from 1 to 25 with a default of 3, but it applies to prompt-type Stop hooks only.
- A user-defined hook returning allow overrides even a global deny. The deny list is only as strong as the weakest hook an Administrator wrote.
The identity underneath: Reader, Privileged and on-behalf-of
Every agent gets a user-assigned managed identity at creation, and you pick a permission level that decides which RBAC roles it receives on the resource groups you select. Reader grants core monitoring roles plus resource-type reader roles. Privileged adds contributor roles for the resource types it detects, such as Container App Contributor. Reader, Log Analytics Reader and Monitoring Reader are always assigned at resource group scope, and Monitoring Contributor at subscription scope so the agent can acknowledge and close Azure Monitor alerts. With no resource groups assigned the identity has no permissions, and you can remove only whole resource groups, never individual permissions.
When the identity lacks a role, the agent asks for temporary access through the Microsoft Entra on-behalf-of flow, using the signed-in person's rights. Only an SRE Agent Administrator can authorise it, personal Microsoft accounts cannot, and the permission is not retained afterwards. A Reader-level agent in Review mode therefore pushes every write through a human administrator's own credentials, which is the strongest pattern in the documentation. The recipes lean that way: the minimal and onboarding lab recipes set accessLevel Low with actionMode Review, while the dynatrace-servicenow recipe sets High with Review. The docs call the levels Reader and Privileged and I found no page mapping Low and High onto them, so reading Low as Reader is my inference.
Connectors add a third surface. Managed connectors let you select which operations the agent can see, lock parameters such as an email recipient to a fixed value, and mark each operation Allow or Ask. The connection uses the configuring user's credentials for every user of the agent, so anyone who can chat with it can invoke the enabled operations as that person.
What to do on Monday
Work from the outside in, identity first, because every rule above sits on top of what the managed identity can already do.
- List the agent's role assignments and confirm no Contributor-class role sits on a production resource group unless you intended it.
- Write the global deny list first: destructive Azure CLI and kubectl patterns, role assignment changes, and the ExecutePythonCode tool wherever the identity can write.
- Audit every response plan and scheduled task for its run mode. The docs recommend Review for production incidents and security alerts, Autonomous for daily health checks and cost reports, and say to observe for two to four weeks before promoting a trigger.
- For each Autonomous trigger, list every tool or connector operation set to Ask. Each one runs unattended, so move it to deny or remove it from that trigger's configuration.
- Write approval hooks with a reason on every rejection, failMode block, and a command hook where the pattern is deterministic enough to match without an LLM.
# 1. What can the agent's managed identity actually do today?
# Find the identity in the agent portal: Settings > Azure settings > Go to Identity.
az role assignment list --assignee <AGENT_MANAGED_IDENTITY_ID> --all \
--query "[].{role:roleDefinitionName, scope:scope}" -o table
# 2. If a Contributor-class role shows up on a production resource group and
# you did not mean it, the documented way to read-only is to hold Reader
# and let the agent ask for on-behalf-of access when it must write.
az role assignment create \
--assignee <AGENT_MANAGED_IDENTITY_ID> \
--role Reader \
--scope /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>
# 3. Per-trigger run mode lives on the response plan or scheduled task
# (Agent autonomy level), not on the agent. Review the list of triggers
# before you review any rule.None of this needs the preview features. It needs one afternoon with the policy file, the trigger list and the role assignments open side by side.
An agent that is safe to leave on call is one where the things it must never do are denied at global scope, the things it may do alone are read-only, and every write passes through a person's credentials. Everything between those is Ask, and Ask only holds while a human is watching. Design the deny list as if no one will ever be there.
Sources and further reading
- Microsoft Learn: Tool access policies in Azure SRE Agent
- Microsoft Learn: Run modes in Azure SRE Agent
- Microsoft Learn: Agent hooks in Azure SRE Agent
- Microsoft Learn: Agent permissions in Azure SRE Agent
- Microsoft Learn: Managed connectors in Azure SRE Agent (preview)
- GitHub: microsoft/sre-agent repository
- GitHub: onboarding lab tool-permissions.json
- GitHub: deployment compliance approval hook

