-
Notifications
You must be signed in to change notification settings - Fork 2k
Add agent-approval-check composite action #1429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
45fe820
Add agent-approval-check composite action
km-anthropic 87354dc
Drop stray internal acronym from comment
km-anthropic 461871a
agent-approval-check: require write-access approvers, pin deps, pagin…
OctavianGuzu 48ad1e9
agent-approval-check: prettier
OctavianGuzu ce277f6
agent-approval-check: verify approver write permission via REST; comm…
OctavianGuzu a95bc50
agent-approval-check: use headRefOid; drop pull_request_review trigge…
OctavianGuzu 9de47cf
agent-approval-check: stale-notification wording, no-retry-on-4xx, do…
OctavianGuzu ec008fb
agent-approval-check: fail-closed sibling guard on commits-ordering e…
OctavianGuzu e438f30
agent-approval-check: drop hardcoded API-call counts from logs; clari…
OctavianGuzu 613379d
agent-approval-check: count all agent-email commits (close-reopen byp…
OctavianGuzu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| (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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: "" | ||
|
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" | ||
|
claude[bot] marked this conversation as resolved.
|
||
| - run: pip install 'httpx==0.28.1' 'pyyaml==6.0.3' 'tenacity==9.1.4' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/] |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.