Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions agent-approval-check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Agent Approval Check

Require **N human approvals** on any pull request that contains commits
authored by an AI agent (Claude, Claude Code, or any bot identity you
configure). PRs without agent activity are unaffected.

This is the same gate Anthropic runs internally on every agent-authored PR.

## What it does

When a PR is opened, pushed to, or commented on, this action:

1. Scans the PR's commits, author, and reviews for the configured agent
identities (committer email, bot login, or an `APPROVED` review from a
bot). If none are found it posts `success: No agent activity` and stops.
2. Counts distinct human approvals: the latest `APPROVED` review per login,
plus any `/approve <head-sha>` comment whose SHA matches the current
head. Only users with write access to the repo count (verified per-user
via the collaborators permission API); agent and excluded-bot logins
never count.
3. Posts an `agent-approval-check` commit status (`success` once the count
reaches `required_approvals`, otherwise `pending`) and a sticky PR
comment explaining what's still needed.
4. Re-evaluates on every new push or comment. A push moves the head SHA,
so earlier `/approve <old-sha>` comments are flagged stale. Approving
reviews still count toward the threshold — they're picked up the next
time the workflow runs (on push or `/approve`); they just don't trigger
a run on their own.

Mark `agent-approval-check` as a **required status check** on your protected
branches and GitHub will refuse to merge until it's green.

## Setup

Copy [`examples/agent-approval-check.yml`](../examples/agent-approval-check.yml)
into `.github/workflows/` in your repo, then add `agent-approval-check` to the
required status checks on your protected branch.

This action is designed to run **alongside** GitHub's native branch
protection, not replace it. On the same protected branch you should also:

1. Require at least 1 approving review from someone with write access.
2. Enable **Dismiss stale pull request approvals when new commits are pushed**.

```yaml
name: agent-approval-check
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
statuses: write
jobs:
check:
if: github.event_name != 'issue_comment' || github.event.issue.pull_request
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action/agent-approval-check@main
with:
required_approvals: 2
agent_emails: noreply@anthropic.com
agent_logins: claude[bot],claude-code[bot]
```

## Inputs

| Input | Default | Meaning |
| ---------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `required_approvals` | `2` | Distinct human approvals needed. |
| `agent_emails` | `noreply@anthropic.com` | Committer emails that mark a commit agent-authored. |
| `agent_logins` | `claude[bot],claude-code[bot]` | Logins treated as agents (PR author or approving reviewer). |
| `excluded_approvers` | _(empty)_ | Logins whose approvals never count. |
| `exempt_head_branches` | _(empty)_ | Head-branch globs that auto-pass. ⚠️ Leave empty — branch names are attacker-controlled, so this is not a safe place to encode trust. |
| `exempt_path_prefixes` | _(empty)_ | PRs touching only these prefixes auto-pass. |
| `protected_bases` | _(default branch)_ | Base branches this check gates (see threat model). |
| `config_file` | _(empty)_ | Path to an [agent-identities YAML](./agent-identities.example.yaml) replacing the inline inputs. See the warning below. |
| `docs_url` | this README | Link in the PR comment footer. |
| `github_token` | `${{ github.token }}` | Needs `statuses:write` + `pull-requests:write`. |

> ⚠️ **`config_file` and checkout:** if you set `config_file`, your workflow
> must check out the **base** branch to read it (the default behaviour of
> `actions/checkout` under `pull_request_target`). Never check out the PR
> head ref — doing so would let the PR author control the config and bypass
> this check.

## Approving

A human counts as an approver by either:

- submitting a normal GitHub **Approve** review, or
- commenting `/approve <sha>` where `<sha>` is the current head commit
Comment thread
OctavianGuzu marked this conversation as resolved.
(12–40 hex chars). This path lets the PR author — who can't approve their
own PR in GitHub's UI — vouch for commits an agent pushed on their behalf.
The author's `/approve` is subject to the same write-access verification
as any other approver, so a fork-PR author without write access on the
base repository cannot self-count. The author counts as **one** approval;
the remaining approvals must come from other reviewers with write access.

## Threat model

- **Tamper-proof triggers.** `pull_request_target` and `issue_comment` run
the workflow file from the base/default branch, so the PR under review
cannot edit this check. `pull_request_review` does **not** share this
property — it runs from the merge ref — so the example workflow omits it;
native Approve reviews are picked up on the next synchronize or
`/approve` comment. This tamper-resistance assumes the workflow file
itself is protected: an actor who can push workflow changes to the
default branch can spoof any required status check, including this one,
so protect `.github/workflows/` via branch protection or CODEOWNERS.
- **Fail-closed.** Any unhandled error exits non-zero; the required status
stays non-success and the PR stays blocked. PRs with >100 commits are
treated as agent-authored because the full commit list can't be verified.
- **Sibling-PR guard.** Commit statuses attach to a SHA, not a PR. The
action refuses to post a status on a PR whose base isn't in
`protected_bases`, and withholds `success` while another open PR to a
protected base shares the same head commit — otherwise a green status on
one PR would also unblock the other.
- **No checkout of PR code.** The action never checks out the PR's branch;
it reads PR metadata via the GitHub API, so the usual
`pull_request_target` code-execution risk does not apply.
72 changes: 72 additions & 0 deletions agent-approval-check/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Agent Approval Check
description: |
Require N human approvals on PRs that contain agent-authored commits
(Claude, Claude Code, or any configured bot identity). Posts an
`agent-approval-check` commit status — mark it as a required check on
protected branches to gate merges.

inputs:
github_token:
description: Token with statuses:write and pull-requests:write on this repo.
default: ${{ github.token }}
required_approvals:
description: Number of distinct human approvals required. Must be >= 1.
default: "2"
agent_emails:
description: Comma-separated committer emails treated as agent-authored.
default: noreply@anthropic.com
agent_logins:
description: |
Comma-separated GitHub logins treated as agents — a PR opened by, or an
APPROVED review from, one of these triggers the check.
default: claude[bot],claude-code[bot]
excluded_approvers:
description: Comma-separated logins whose approvals never count (e.g. rubber-stamp bots).
default: ""
exempt_head_branches:
description: |
Comma-separated glob patterns; PRs from matching head branches auto-pass.
WARNING: leave empty — branch names are attacker-controlled, so this is
not a safe place to encode trust.
default: ""
Comment thread
claude[bot] marked this conversation as resolved.
exempt_path_prefixes:
description: Comma-separated path prefixes; PRs touching only these auto-pass.
default: ""
protected_bases:
description: |
Comma-separated base branches this check gates. Empty = the repo's
default branch only. PRs targeting any other base are refused (no
status posted) so a sibling PR sharing the head SHA can't get the
shared commit stamped green.
default: ""
config_file:
description: Optional path to an agent-identities YAML file (overrides the inline inputs).
default: ""
docs_url:
description: Link shown in the PR comment footer.
default: "https://github.com/anthropics/claude-code-action/tree/main/agent-approval-check"

runs:
using: composite
steps:
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
Comment thread
claude[bot] marked this conversation as resolved.
- run: pip install 'httpx==0.28.1' 'pyyaml==6.0.3' 'tenacity==9.1.4'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are 3 pretty common libraries that could/should be hash pinned for the commit given that we're fully specifying the semantic version.

shell: bash
- run: python "${{ github.action_path }}/agent_approval_check.py"
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
GH_REPOSITORY: ${{ github.repository }}
GH_EVENT_NAME: ${{ github.event_name }}
GH_EVENT_PATH: ${{ github.event_path }}
REQUIRED_APPROVALS: ${{ inputs.required_approvals }}
AGENT_EMAILS: ${{ inputs.agent_emails }}
AGENT_LOGINS: ${{ inputs.agent_logins }}
EXCLUDED_APPROVERS: ${{ inputs.excluded_approvers }}
EXEMPT_HEAD_BRANCHES: ${{ inputs.exempt_head_branches }}
EXEMPT_PATH_PREFIXES: ${{ inputs.exempt_path_prefixes }}
PROTECTED_BASES: ${{ inputs.protected_bases }}
CONFIG_FILE: ${{ inputs.config_file }}
DOCS_URL: ${{ inputs.docs_url }}
33 changes: 33 additions & 0 deletions agent-approval-check/agent-identities.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
# Optional config-file form of the agent-approval-check inputs.
# Pass via `with: { config_file: .github/agent-identities.yaml }` instead of
# the inline `agent_emails` / `agent_logins` / … inputs.

# Committer emails that mark a commit as agent-authored.
agent_emails:
- noreply@anthropic.com

# GitHub logins treated as agents — a PR opened by, or an APPROVED review
# from, one of these triggers the check.
agent_app_logins:
- claude[bot]
- claude-code[bot]

# Logins whose approvals never count toward the required total.
excluded_approver_logins: []

# Head-branch glob patterns that auto-pass. Leave empty: branch names are
# attacker-controlled, so this is not a safe place to encode trust.
exempt_head_branches: []

# Per-repo path prefixes whose PRs auto-pass when ONLY those paths change.
exempt_path_prefixes:
owner/repo:
- docs/

# Per-repo base branches this check gates. A repo with no entry defaults to
# its default branch only. Listing a repo here REPLACES that default.
protected_bases:
owner/repo:
exact: [main]
prefixes: [release/]
Loading
Loading