Skip to content

Review human AFF3 #11873

Review human AFF3

Review human AFF3 #11873

Workflow file for this run

name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
# `edited` is not a trigger here, but a comment can still arrive while a run is
# in flight; keep one agent per issue/PR.
concurrency:
group: claude-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
jobs:
claude:
# Only a repo OWNER/MEMBER/COLLABORATOR can dispatch the agent. Without this,
# any GitHub user could drive a write-capable agent by writing "@claude" in a
# comment on a public repo.
#
# For `issues: assigned`, this deliberately gates on the *issue author's*
# association rather than the assigner's: assigning an externally-authored
# issue should not turn untrusted issue text into an agent trigger.
if: |
(
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@claude') &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
) ||
(
github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
) ||
(
github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)
) ||
(
github.event_name == 'issues' &&
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
)
runs-on: ubuntu-latest
# A GitHub App installation token expires 60 minutes after minting and does
# not refresh; stay inside that window.
timeout-minutes: 55
permissions:
# Scopes the default GITHUB_TOKEN only; the agent works with the ai4c-agent
# App token below.
contents: read
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Generate ai4c-agent token
id: ai4c-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.AI4C_AGENT_APP_ID }}
private-key: ${{ secrets.AI4C_AGENT_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
token: ${{ steps.ai4c-token.outputs.token }}
# This agent has Bash(*); do not leave a token in .git/config for it
# to read. git authenticates via the gh credential helper below.
persist-credentials: false
- name: Configure git identity and credential helper
env:
GH_TOKEN: ${{ steps.ai4c-token.outputs.token }}
APP_SLUG: ${{ steps.ai4c-token.outputs.app-slug }}
run: |
APP_USER_ID="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)"
git config --global user.name "${APP_SLUG}[bot]"
git config --global user.email "${APP_USER_ID}+${APP_SLUG}[bot]@users.noreply.github.com"
gh auth setup-git
- name: Resolve agent config
uses: ./.github/actions/resolve-agent-config
with:
workflow: claude
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install python tools
run: |
uv sync
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
env:
GH_TOKEN: ${{ steps.ai4c-token.outputs.token }}
GITHUB_TOKEN: ${{ steps.ai4c-token.outputs.token }}
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.ai4c-token.outputs.token }}
# Lets Claude read CI results on PRs.
additional_permissions: |
actions: read
# No `prompt:` on purpose. This is the interactive "@claude" responder,
# and v1 switches to non-interactive automation mode as soon as a prompt
# is supplied. Repository conventions come from CLAUDE.md, which the
# agent reads; they used to be duplicated here in a `custom_instructions:`
# block that v1 does not accept and silently discarded.
#
# `mcp_config:`, `model:` and `allowed_tools:` were dropped the same way
# — none of them are v1 inputs. Everything now goes through claude_args,
# which is how v1 takes CLI flags. Both flags ACCUMULATE with tag mode's
# own values rather than replacing them, so the comment-update and CI
# MCP tools survive.
#
# The old file asked for `Bash(*),Edit,MultiEdit,Write`, but since the
# input was being discarded it never took effect — the agent has been
# running on tag mode's narrow set. Restoring that literally would grant
# blanket write for the first time, in the same commit that calls this
# workflow out as the incident shape, so it is scoped to what the
# gene-review protocol needs instead. Edit/MultiEdit/Write are
# deliberately absent: tag mode runs `--permission-mode acceptEdits`,
# which already permits edits inside the workspace, and listing the
# tools would extend that to the whole runner.
#
# BE CLEAR ABOUT WHAT THIS LIST IS AND IS NOT. It is not a confinement
# boundary: `Bash(uv:*)` covers `uv run python -c '...'`, `Bash(uvx:*)`
# runs an arbitrary PyPI package, and `gh alias set --shell` is a shell
# escape. Arbitrary execution is ACCEPTED for this agent, because only
# OWNER/MEMBER/COLLABORATOR can trigger it (see the job `if:`) and it
# holds a token that expires with the job. What the list actually buys
# is workspace-scoped writes and the removal of `Bash(*)`.
#
# Git is the one exception worth spelling out, because it has a
# documented escape rather than an implied one: a `Bash(git:*)` prefix
# rule permits `git push --receive-pack='sh -c ...' ext::sh`, which is
# exactly the RCE the action's vendored scripts/git-push.sh wrapper
# exists to close (HackerOne #3556799). Tag mode already unions in
# `git add`/`git commit`/`git rm` and that wrapper, so only read-only
# verbs were missing and only those are added. `git fetch` is NOT among
# them despite looking read-only: `git fetch 'ext::sh -c <cmd>'` runs
# <cmd> locally, since protocol.ext.allow defaults to `user`.
claude_args: |
--model ${{ env.AGENT_MODEL }}
--mcp-config '{"mcpServers": {"ols": {"command": "uvx", "args": ["ols-mcp"]}, "sequential-thinking": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]}}}'
--allowedTools "Read,Glob,Grep,WebSearch,WebFetch,Bash(just:*),Bash(uv:*),Bash(uvx:*),Bash(gh:*),Bash(git status:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git checkout:*),mcp__ols__search_all_ontologies,mcp__ols__get_terms_from_ontology,mcp__ols__get_ontology_info,mcp__sequential-thinking__sequentialthinking"
- name: Write step summary
# Not `always()`: with cancel-in-progress a superseded run writes no
# execution log, and the summary would report that as breakage.
if: ${{ !cancelled() }}
uses: ./.github/actions/agent-run-summary
with:
execution-file: ${{ steps.claude.outputs.execution_file }}
title: "@claude Run"