Skip to main content
BlogCloud

Azure SRE Agent: Tool Permissions, Hooks and Approval Gates

How Azure SRE Agent stacks hooks, allow/ask/deny tool policies, connector approvals and run modes, where Ask quietly stops asking, and what to configure first.

Matthews Wong

Software Engineer

October 202612 min read
Azure SRE Agent: Tool Permissions, Hooks and Approval Gates

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:

PriorityControlWhat it can decideWho sets it
HighestHooksAllow, block or ask per tool call. A hook allow skips everything below itSRE Agent Administrators only
SecondTool access policiesAllow, ask or deny by tool-name globGlobal: admin. Custom agent: admin or author. Thread: any user
ThirdDefault approvalConnector Ask and RequiresApproval tools pause for a personWhoever configures the connector
LowestRun modesReview pauses, Autonomous auto-approvesPer 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: 3

Read 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 matchedIn Review modeIn Autonomous mode
Global denyBlockedBlocked
Allow at any scopeRuns immediatelyRuns immediately
Global askUser approvesAuto-approved
No policy match, connector tool set to AskUser approvesAuto-approved
No policy match, nothing else appliesExecutesExecutes

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.

  1. List the agent's role assignments and confirm no Contributor-class role sits on a production resource group unless you intended it.
  2. Write the global deny list first: destructive Azure CLI and kubectl patterns, role assignment changes, and the ExecutePythonCode tool wherever the identity can write.
  3. 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.
  4. 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.
  5. 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.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is the difference between Review and Autonomous mode in Azure SRE Agent?

In Review the agent proposes an action and an SRE Agent Administrator approves or denies it. In Autonomous it executes immediately and reports back. Run mode is set per response plan and scheduled task, and Review shows approval buttons only for Azure infrastructure operations.

How do allow, ask and deny rules work in Azure SRE Agent?

They are glob patterns on tool names, set at global, custom agent or thread scope. Evaluation runs deny, then allow, then ask, then default behaviour, and allow beats ask. Only the global scope can deny, so narrower scopes can never weaken a global deny.

Does an Ask rule still protect me in Autonomous mode?

No. Microsoft's managed connectors page says every tool set to Ask executes without human approval in Autonomous mode, including scheduled tasks, HTTP triggers and incident auto-response. Put anything that must never run unattended in a global deny rule instead.

What events do Azure SRE Agent hooks support?

The hooks page lists two: Stop, which fires when the agent is about to return its final response, and PostToolUse, which fires after a tool finishes successfully. Hooks can be LLM prompts or sandboxed bash or Python scripts. A Stop hook rejection without a reason is treated as approval.

How do I stop an Azure SRE Agent from changing production?

Combine layers: give its managed identity Reader so writes need on-behalf-of authorisation from an administrator, keep production triggers in Review, and deny destructive commands globally. Name ExecutePythonCode explicitly, because the bash alias does not cover Python.