- 6.1 Define autonomy levels — classify actions by operational, security and compliance risk; assign autonomy that is fast and compliant
- 6.2 Implement guardrails and human-in-the-loop workflows — actions needing judgement, blocking violations, least privilege, controlled paths for irreversible changes, preserving velocity
6.1 Define autonomy levels
Classify agent actions by risk
Risk-based autonomy means an agent may act within boundaries that match the impact and reversibility of what it is doing. Risk is not only a property of the action; it depends on where the change lands, how easily it can be reversed, and how quickly it reaches users or systems. Classify along three lenses:
| Lens | Ask | Raises the risk |
|---|---|---|
| Operational | What breaks if this is wrong, and how fast can we undo it? | Production deploys, infrastructure, database migrations, workflow changes |
| Security | What could this expose or grant? | Secrets, permissions, authentication code, new dependencies, network egress, MCP servers with write tools |
| Compliance | Does this touch regulated data or a controlled process? | PII, audit evidence, licence changes, change-management records, retention |
| Risk level | Example actions | Recommended control |
|---|---|---|
| Low | Docs, formatting, comments, test additions | Full automation after checks (auto-merge) |
| Medium | Dependency updates, safe refactors, feature code | PR + required checks + at least one review |
| High | Infrastructure, workflows, auth, agent configuration | CODEOWNERS + explicit approvals + stronger checks |
| Critical | Production deploy, production secret access, permission changes | Environment gate + named reviewers + audit evidence required |
Small diff, big consequence
Treat .github/workflows/, infra/ and security/ as high risk by default — and,
for agent systems, the agent configuration itself: .github/agents/, .github/hooks/,
.github/copilot-instructions.md, copilot-setup-steps.yml and MCP settings. A one-line change
there alters what every future agent run is told or allowed to do.
Assign autonomy levels
| Autonomy level | The agent may | Typical implementation |
|---|---|---|
| Read-only | Inspect, summarize, classify, recommend | Read and search tools only; read-only MCP; contents: read |
| Propose-only | Create branches and PRs; never merge or deploy | Copilot cloud agent's default; rulesets requiring review |
| Execute with guardrails | Run pre-approved workflows: tests, builds, staging deploys | Allowed workflows; auto-approved workflow runs where safe; staging environment without reviewers |
| Human-authorized execution | High-impact actions only after a person approves | Environment required reviewers; plan-first; CODEOWNERS |
Autonomy is never one setting — it is the combination of PR rules, environments, workflow permissions and tool access. And it should be routed by a machine-readable signal, not by someone reading a narrative:
flowchart LR
PLAN["Agent plan<br/>plan.json with risk rating"] --> ROUTE{"risk"}
ROUTE -->|"low"| AUTO["apply_auto<br/>runs after checks"]
ROUTE -->|"medium · high"| GATED["apply<br/>environment: approval-required"]
GATED --> APPROVER["Required reviewer<br/>approves or rejects"] name: agent-plan-apply
on:
workflow_dispatch:
permissions:
contents: read
jobs:
plan:
runs-on: ubuntu-latest
outputs:
risk: ${{ steps.read.outputs.risk }}
steps:
- uses: actions/download-artifact@v8
with:
name: plan
path: out
- id: read
run: echo "risk=$(jq -r .risk out/plan.json)" >> "$GITHUB_OUTPUT"
apply_auto:
needs: plan
if: needs.plan.outputs.risk == 'low'
runs-on: ubuntu-latest
steps:
- run: ./scripts/apply.sh out/plan.json
apply:
needs: plan
if: needs.plan.outputs.risk != 'low'
runs-on: ubuntu-latest
environment: approval-required # pauses for a required reviewer
steps:
- run: ./scripts/apply.sh out/plan.json Grow autonomy progressively
- The agent can only open PRs; it cannot merge.
- It may auto-merge low-risk PRs once checks pass.
- It may deploy to staging automatically.
- It reaches production only through an environment approval.
Each step is earned with evidence from the previous one. The same logic applies to issue triage automations, whose automation level ranges from Full control (every change held for review) through Cautious (the default: only high-confidence changes apply) and Balanced to Full automation.
Stay compliant with security and Responsible AI standards
Organizational security standards and Responsible AI principles meet in the same practical controls: people remain accountable for what agents do (co-authored commits, requesters cannot self-approve); work stays transparent (session logs, plans, audit events); humans keep oversight of consequential decisions (reviews, environments); and exposure is minimized (least privilege, firewall, secrets kept out of the agent's reach). An autonomy level that cannot be audited is not compliant, however fast it is.
6.2 Implement guardrails and human-in-the-loop workflows
Identify the actions that require human judgement
Put people at decision points, not at every step:
- Merging into a protected branch.
- Deploying to production, or any irreversible data operation.
- Accessing secrets or changing permissions and tokens.
- Changing workflows, agent configuration or security settings.
- Accepting a security exception, a licence change or anything with legal weight.
- Choosing between conflicting agent outputs, or resolving ambiguity the task did not settle.
flowchart LR
A["Agent opens PR<br/>with plan and risk level"] --> C["Checks validate"]
C --> O["CODEOWNERS<br/>auto-requested"]
O --> M{"Merge<br/>decision point"}
M --> D{"Deploy<br/>decision point"}
D --> P["Production<br/>environment approval"] Block actions that violate policy
Blocking must be enforced by the platform. "Controls in docs only" is an anti-pattern: if the platform cannot enforce it, it will eventually be bypassed. Layer the controls so each catches what the previous one missed.
flowchart LR PRE["Before the action<br/>permissions · tool allowlists · firewall"] --> DURING["During the action<br/>preToolUse hooks · required checks · push protection"] DURING --> AFTER["After the action<br/>reviews · environments · audit log · artifacts"]
| Control | Blocks |
|---|---|
| Rulesets / branch protection | Direct pushes, force pushes, branch deletion, merges without a PR, required checks, required approvals, code-owner review, code scanning results |
| CODEOWNERS + "require review from Code Owners" | Changes to sensitive paths without the owning team's approval |
preToolUse hook | A specific tool call, deterministically, before it runs — returns permissionDecision: "deny" with a reason |
| Push protection | Supported secrets before they are committed |
| Agent firewall | Outbound network requests to hosts not on the allowlist |
Copilot CLI --deny-tool; managed settings | Tool use on developer machines; allow-all modes when disabled by enterprise policy |
MCP allowlists (deniedMcpServers) | Unapproved MCP servers in IDEs and CLI |
| Copilot policies | Cloud agent, third-party agents, automations or MCP per organization or enterprise — each managed separately |
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": "./.github/hooks/scripts/block-dangerous.sh",
"timeoutSec": 10
}
],
"postToolUse": [
{
"type": "http",
"url": "https://audit.contoso.com/copilot",
"timeoutSec": 5
}
]
}
} #!/bin/bash
# Reads the preToolUse payload on stdin and denies destructive shell commands.
input=$(cat)
tool=$(jq -r '.toolName' <<< "$input")
args=$(jq -r '.toolArgs' <<< "$input")
if [[ "$tool" == "bash" ]] && grep -Eq 'rm -rf /|git push --force|curl [^|]*\| *(ba)?sh|sudo ' <<< "$args"; then
jq -cn --arg r "Blocked by policy: destructive or remote-execution command" \
'{permissionDecision: "deny", permissionDecisionReason: $r}'
exit 0
fi
echo '{"permissionDecision": "allow"}' How hooks fail
A preToolUse command hook is fail-closed: exit code 2, a crash or any
other non-zero exit denies the tool call, even if its output says "allow". But timeouts are always
fail-open — a slow hook lets the call proceed. HTTP preToolUse hooks are
fail-open on network errors. If any hook returns "deny", the tool is blocked. Under cloud agent,
"ask" is treated as "deny" because no one is there to answer. Hook files must be on the
default branch to be used by cloud agent.
Some rules cannot be expressed as settings alone. A defence-in-depth guardrail workflow, made a required check, can insist that at least one approval came from a human account rather than an automation identity:
name: agent-guardrails
on:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: read
jobs:
require-human-approval:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v9
with:
script: |
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
const human = reviews.some(r => r.state === 'APPROVED' && r.user?.type === 'User');
if (!human) core.setFailed('A human approval is required before merge.'); Scope permissions and execution contexts for least privilege
Permissions are the true boundary of an agent's power — they decide what it can do regardless of its instructions or intent. Broad tokens turn small mistakes into large incidents.
permissions:
contents: read # workflow default: read-only
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: npm run analyze
open_pr:
needs: analyze
runs-on: ubuntu-latest
permissions: # elevated only for the job that writes
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v7
- run: ./scripts/open-pr.sh | Where | Least-privilege practice |
|---|---|
Workflow GITHUB_TOKEN | Read-only default at the organization or enterprise level; permissions at workflow level; elevate per job; treat pull-requests: write as elevated |
copilot-setup-steps.yml | Minimal permissions — the token is only for setup; Copilot gets its own session token |
| Cloud agent's GitHub access | Default GitHub MCP token is read-only and limited to the current repository; widen only deliberately, on the /readonly URL where possible |
| Secrets | Agents secrets only for what the agent needs; anything it must never see stays in Actions secrets (which it cannot read); production secrets only in the production environment; COPILOT_MCP_ names reach MCP servers only |
| Tools | Explicit tools lists in agent profiles and MCP servers; read-only tools for planning and review agents |
| Copilot CLI | Specific --allow-tool patterns instead of --allow-all; --secret-env-vars to redact variables from shell and MCP environments |
| Runners | GitHub-hosted (fresh VM each time) or ephemeral self-hosted runners |
| Third-party actions | Minimize them and pin versions — by full commit SHA for untrusted sources |
# Read-only analysis: only git commands may run without a prompt
copilot -p "Summarize the last 10 commits and flag breaking changes" -s \
--allow-tool='shell(git:*)' --deny-tool='shell(git push)' --deny-tool=write Require explicit authorization for irreversible or compliance-sensitive changes
GitHub builds several controlled paths into Copilot cloud agent, and you add the rest:
- Only users with write access can trigger cloud agent; comments from others are never shown to it.
- The agent pushes only to its own branch, and only through simple push operations — it cannot run
git pushor other Git commands directly. - Its PRs are drafts; it cannot mark them ready, approve or merge them.
- The person who asked for the PR cannot approve it, preserving "required approvals".
- When a PR is opened under Copilot's own app identity (not attributed to a person), one additional approval is required if the repository already requires at least one — on by default in rulesets, always on for branch protection.
- Actions workflows do not run on its pushes until someone with write access clicks Approve and run workflows — disabling that is a repository-admin decision, and it can expose secrets to unreviewed code.
- Hidden characters and HTML comments are filtered from issues and comments before they reach the agent, to reduce prompt injection.
For the change itself, use environments with required reviewers for deploys and secret access, plan-first for irreversible work, and in GitHub Agentic Workflows, safe outputs so the agent only proposes writes that a separate, gated job performs after threat detection.
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production # required reviewers · production-only secrets
concurrency:
group: production # serialize production deploys
cancel-in-progress: false
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }} # deploy an explicit commit, not "latest"
- run: ./deploy.sh # CODEOWNERS — only enforced when "Require review from Code Owners" is on
* @org/core-team
/security/ @org/security-team
/infra/ @org/platform-team
/.github/workflows/ @org/platform-team
/.github/agents/ @org/ai-governance
/.github/hooks/ @org/ai-governance
/.github/copilot-instructions.md @org/ai-governance
/.github/workflows/copilot-setup-steps.yml @org/ai-governance Preserve execution velocity
An approval is only worth its delay if it materially reduces risk. Approvals everywhere produce governance fatigue and rubber-stamping — which is worse than no approval, because it looks like control.
| Friction that does not reduce risk | Faster, equally safe alternative |
|---|---|
| Manual approval of every docs or formatting PR | Auto-merge after required checks |
| Prompting for every read or search tool | Pre-approve read-only tools; gate edit and execute |
| Reviewers re-deriving context from the diff | Evidence-first PRs: plan, checks, artifacts, rollback note |
| Everyone reviews everything | CODEOWNERS routes each path to the people who know it |
| Huge agent PRs that take hours to review | Small, scoped tasks and PRs |
| Holding all triage changes for review | Automation level Cautious or Balanced: only low-confidence changes wait |
Approvals are not always security controls
For automated issue triage, suggestions awaiting approval are a workflow convenience, not a security boundary: an agent with permission to change issues could apply changes directly through the API. Control what an automation can do with repository and agent permissions; use approvals to decide what it should do.
Keep governance working over time
Controls drift: checks get renamed or removed, review requirements are relaxed, CODEOWNERS goes stale, permissions expand, secrets move to broader scopes. Treat governance as an operational responsibility with a cadence:
| Cadence | Review |
|---|---|
| Weekly | Failed agent runs and common policy violations |
| Monthly | Workflow permissions and secret scopes |
| Quarterly | Rulesets, CODEOWNERS, environment reviewers, evidence retention |
| Anti-pattern | Why it fails | Mitigation |
|---|---|---|
| Unbounded autonomy | Irreversible changes happen without oversight | Environments, required reviewers, rulesets |
| Excess permissions | A small mistake becomes a major incident | Least privilege, environment-scoped secrets, job-level permissions |
| Missing audit trail | You cannot prove what happened | Artifact uploads and evidence-first workflows — missing evidence is a failure |
| Bypass paths | Direct pushes or disabled checks skip policy | Rulesets that restrict pushes; tightly limited bypass lists |
| Rubber-stamping | Approvals become "click to unblock" | Better evidence, CODEOWNERS routing, smaller PRs |
| Ambiguous tasks | An agent executes a guess | If a task cannot be defined precisely — criteria, constraints, paths, validation — it should not run autonomously |
Recovery belongs to governance too
Retry only transient failures, escalate after repeated ones, roll back with small PRs and explicit refs, and investigate security failures instead of retrying them. The file-level syntax for every control on this page is collected in the Configuration reference.