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-continuescaps 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
- About pull request templates: Where the template file goes and how GitHub picks it up.
- Managing environments for deployment: Required reviewers, prevent self-review and environment secrets.
- Custom agents configuration: The
toolsproperty and the tool aliases. - Configure MCP servers for your repository: The JSON format, variable substitution and secrets.
- Hooks reference: Every hook event and its input and output.
- Allowing Copilot CLI to work autonomously: Autopilot, continuation limits and permissions.
- Allowing and denying tool use: The
--allow-tooland--deny-toolpatterns. - About code scanning with CodeQL: What CodeQL analyses and how results reach code scanning.
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.