A coding agent on GitHub takes an issue, creates a branch, commits and opens a pull request. That is useful, and it is also a new actor with write access to your repository. The checks that work for people don't cover it on their own: the agent doesn't get tired, doesn't hesitate, and does exactly what its permissions allow.

Four risks, and the GitHub control for each:

Risk What it looks like Control in GitHub
Planless execution A diff without a plan or rationale PR template with a required plan, review before merge
Over-permissioned agents Workflows that can write to the repo and read every secret Least-privilege GITHUB_TOKEN, environments with required reviewers
Hidden reasoning No scope, assumptions or decision trail Plan in the PR, linked workflow runs, decisions in PR comments
Blind trust in automation "CI passed, ship it" Checks plus CODEOWNERS, required reviews and risk-based approvals

Everything below uses GitHub's own features: templates, workflow permissions, environments, MCP configuration, hooks and code scanning. No extra product.

Where an agent's work leaves evidence

Each stage of the work maps to a place in GitHub where it leaves a record. That is where a reviewer looks, so that is where the controls go.

Stage What the agent does Where the evidence is
Planning Drafts scope, steps and success criteria Issue, PR description and comments, Agents tab
Implementation Creates a branch, commits, opens or updates the PR Branch, commits, pull request
Validation Runs checks, iterates on failures Workflow runs, checks, artifacts
Deployment Usually nothing without approval Environments and deployment approvals

Require a plan in every agent pull request

A pull request template makes every PR, from an agent or a person, carry the same sections:

<!-- .github/pull_request_template.md -->

## Plan (required)
- **Goal:**
- **Scope (paths/files):**
- **Steps:**
  1.
  2.
  3.
- **Success criteria (verifiable):**
  - [ ] Required checks pass
  - [ ] Security signals reviewed (as applicable)
- **Risks and mitigations:**
- **Rollback / escalation plan:**

## Evidence
- Workflow run(s):
- Scan results (if applicable):

## Review checklist
- [ ] Plan reviewed and approved
- [ ] Required reviews satisfied
- [ ] Required checks satisfied

A template is an expectation. A required status check makes it a guarantee. A gate that only checks whether the template file exists proves nothing, because the file is always there once it's committed. The check has to read the description of the pull request itself:

# .github/workflows/plan-gate.yml
name: Plan gate

on:
  pull_request:
    types: [opened, edited, synchronize, reopened]
    branches: [main]

permissions:
  contents: read

jobs:
  require-plan:
    runs-on: ubuntu-latest
    steps:
      - name: Require a plan section in the description
        env:
          BODY: ${{ github.event.pull_request.body }}
        run: |
          if ! grep -q '^## Plan' <<< "$BODY"; then
            echo "The pull request description needs a '## Plan' section."
            exit 1
          fi

The description goes through an environment variable, not straight into the script, so text in a PR body can't inject shell commands. Make require-plan a required check in the branch protection for main.

Make the GITHUB_TOKEN read-only by default

Start every workflow read-only and give write access only to the job that needs it:

name: agent-pr

on:
  pull_request:

permissions: read-all

jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Read-only analysis
        run: echo "Analyse PR content"

  update_artifacts:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v7
      - name: Update generated artifacts
        run: echo "Regenerate"
Permission Where Why
read-all Whole workflow The default token can't change anything
contents: write The one job that commits Committing generated files
pull-requests: write Only the job that opens or updates a PR Creating or changing pull requests
issues: write Only when needed Not required just to open a PR

The analysis job never gets contents: write. For a workflow to open pull requests at all, Allow GitHub Actions to create and approve pull requests has to be on in the repository or organisation settings. That setting lives outside the YAML, so it's easy to forget in a review.

Put a human between plan and execution

When an agent runs in a workflow, split planning from execution and put an environment in between. The plan job writes the plan and uploads it. The execute job waits for it and for a human:

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - name: Generate plan
        run: ./agent plan > plan.md
      - name: Upload plan
        uses: actions/upload-artifact@v7
        with:
          name: agent-plan
          path: plan.md

  execute:
    needs: [plan]
    runs-on: ubuntu-latest
    environment: plan-review
    steps:
      - name: Execute approved plan
        run: ./agent execute

The environment plan-review does the work:

  • Required reviewers: a team that may approve plans. The job waits until one of them does.
  • Prevent self-review: whoever started the run can't approve it.
  • Environment secrets: the agent's API keys live in the environment, so they're only available after the protection rules pass.

needs: [plan] controls the order. If the plan job fails, the execute job doesn't run.

The same idea works inside the editor. A custom planning agent in VS Code gets only read tools, and its handoff to the implementing agent is prefilled but not sent, so a person decides when implementation starts:

name: planner-agent
description: Planning
tools: ['search', 'read', 'fetch']
handoffs:
  - label: Start Implementation
    agent: implementer
    prompt: Now implement the plan outlined above.
    send: false

Allowlist MCP tools instead of enabling all of them

Copilot cloud agent uses the MCP tools configured for a repository on its own and doesn't ask before calling them. In a custom agent profile, leaving out tools or setting tools: ["*"] enables everything, including every tool of every configured MCP server. Name the tools instead:

{
  "mcpServers": {
    "issues": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "tools": ["list_issues", "get_issue"],
      "headers": {
        "Authorization": "Bearer $COPILOT_MCP_ISSUES_TOKEN"
      }
    }
  }
}
  • Prefer read-only tools. A write tool on the list is a write the agent can make without asking.
  • Keys never go in the JSON. Store them as Agents secrets on the repository or organisation. Only names that start with COPILOT_MCP_ reach the MCP configuration.
  • The cloud agent and Copilot code review only use MCP tools. Resources and prompts from the server are ignored.
Scope Where the configuration lives
Shared with the team in VS Code .vscode/mcp.json in the repository
Personal The user-profile mcp.json, not committed
Copilot cloud agent Repository Settings → Copilot → MCP servers
A custom agent .github/agents/<agent>.agent.md

Hooks: say no before, report after

Hooks run your own command at fixed points in an agent session. Copilot CLI and the cloud agent both load them from .github/hooks/*.json.

preToolUse runs before a tool call and can block it. Point it at a script that lets ordinary commands through and denies patterns like sudo, rm -rf / or curl … | bash:

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      {
        "type": "command",
        "bash": ".github/hooks/block-dangerous-bash.sh"
      }
    ]
  }
}

To block, the script prints one JSON object (or exits with code 2):

{
  "permissionDecision": "deny",
  "permissionDecisionReason": "Destructive or download-and-execute command needs manual approval."
}

A hook that times out fails open: the tool call goes ahead. Keep the script fast, and don't make it the only control.

For notifications, pick the hook that matches the event:

Hook Runs
errorOccurred Only when an error happens during the agent run
postToolUse After every single tool call
agentStop When the main agent finishes a turn
sessionEnd When the whole session ends: done, error or timeout

An alert for failed runs belongs on errorOccurred, not on sessionEnd.

Limit autopilot in Copilot CLI

Autopilot lets Copilot CLI keep working through a task without waiting for input after each step. It fits well-defined, multi-step work: tests, refactoring, fixing CI failures, batch changes.

copilot --autopilot --max-autopilot-continues 10 -p "Update dependencies, run tests, and open a pull request if anything changed."
  • --max-autopilot-continues caps the automatic continuations, so a loop stops on its own. Every continuation also costs AI credits, whether you're watching or not.
  • --allow-all, or its alias --yolo, lets the agent use every tool, path and URL without asking. With that, it can change or delete files across the repository. Use it only in a sandbox.
  • Between the two, allow and deny specific tools. Deny rules always win, even over --allow-all:
copilot --available-tools='bash,edit,view,grep,glob' --allow-tool='shell(git:*)' --deny-tool='shell(git push)'

This session can explore, edit and commit, but it can't push, reach the internet or start subagents.

Scan what the agent writes

An agent PR goes through the same tests and code scanning as any other. Unit tests and CodeQL in one workflow:

name: tests-and-codeql

on:
  pull_request:

permissions:
  contents: read
  actions: read
  security-events: write

jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Run unit tests
        run: npm test
      - uses: github/codeql-action/init@v4
        with:
          languages: javascript-typescript
      - uses: github/codeql-action/analyze@v4

analyze uploads the SARIF results to code scanning on its own, so don't add upload-sarif unless a second scanner produces its own SARIF. security-events: write is the only write permission the job needs.

Green checks don't replace a review. CODEOWNERS and required reviews make sure a person who knows the code reads the diff and the plan.

Further reading

Related topics: GitHub Copilot, GitHub Actions, Model Context Protocol.

Set up a GitHub repo where a coding agent can't merge without you

Every agent PR carries a plan and waits for your approval, using only GitHub's own settings: a required plan, a read-only token, a human approval gate, allowlisted MCP tools and hooks that say no.