Skip to content
Open
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
116 changes: 116 additions & 0 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Claude PR Review

# Read-only PR review by Claude Code.
#
# ┌─────────────────────────────────────────────────────────────────────────┐
# │ TEMPORARY TEST TRIGGER — remove before merging to main. │
# │ │
# │ The intended trigger is `issue_comment` (comment "/claude-review" on a │
# │ PR). But `issue_comment` ONLY runs the workflow version on the DEFAULT │
# │ branch, so it cannot be tested from this feature branch. `pull_request` │
# │ runs the workflow from the PR branch itself, so we use it here to │
# │ validate the action end-to-end before merge. Scoped to `branches: [main]` │
# │ so it only fires for PRs targeting main. Swap back to the commented-out │
# │ `issue_comment` block below before merging. │
# └─────────────────────────────────────────────────────────────────────────┘
#
# Scope is deliberately minimal (constraint: Claude may READ the repo + PR and
# ADD comments, but must NEVER write, commit, or push):
# * The job `permissions:` block grants only `pull-requests: write` and
# `contents: read`. Because `contents: write` is absent, the GITHUB_TOKEN
# itself is physically incapable of pushing a commit — this is the hard
# enforcement boundary, independent of anything Claude decides to do.
# * `--allowedTools` is defense-in-depth on top: Claude is only handed
# read + comment tools, so it never even attempts a write.
#
# Minutes budget (3000/mo action limit): `timeout-minutes` caps a runaway run,
# and `concurrency` cancels a superseded review so repeat pushes don't stack.
# See .github/CLAUDE_REVIEW.md for the automation notes.

on:
# TEMPORARY test trigger — see banner above. Remove before merge.
pull_request:
types: [opened, reopened, synchronize]
branches: [main]

# INTENDED trigger — uncomment before merge, and re-enable the
# `github.event.issue.pull_request` guard + `issue.number` references below.
# issue_comment:
# types: [created]

# Only one review per PR at a time; a newer trigger cancels the older run
# instead of burning minutes on both.
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
review:
# TEMPORARY guard for the pull_request test trigger: skip drafts and
# Dependabot so we don't waste minutes on WIP/bot PRs.
# For the intended issue_comment trigger, replace with:
# github.event.issue.pull_request &&
# contains(github.event.comment.body, '/claude-review')
if: >
github.event.pull_request.draft == false &&
github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 15

# Gate 2 (the real enforcement): least-privilege token.
# No `contents: write` => no ability to commit or push, ever.
permissions:
contents: read # read the repo/diff; NOT write
pull-requests: write # post the review comment
issues: write # sticky comment lives on the issue timeline

steps:
- name: Checkout PR head
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Run Claude review
uses: anthropics/claude-code-action@v1
with:
# Subscription OAuth token (not API-billed). Generate locally
# with `claude setup-token` and store as a repo/environment
# secret named CLAUDE_CODE_OAUTH_TOKEN.
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

# Ephemeral, least-privilege token from the permissions block
# above. This is what actually caps Claude's GitHub reach.
github_token: ${{ secrets.GITHUB_TOKEN }}

# TEMPORARY: required for the pull_request test trigger so the
# action runs in tracking-comment mode and posts a review
# (without a @mention to react to). Not needed for the
# issue_comment trigger — remove when reverting.
track_progress: true

prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}

You are reviewing this pull request. You have READ-ONLY
access: you may read the repository and PR diff and post a
single review comment. You cannot and must not attempt to
commit, push, edit files, or approve/merge.

Use the GitHub tools to fetch the PR diff and changed
files, then post ONE structured review comment covering:
- Correctness bugs and edge cases
- Security concerns
- Maintainability / readability
- Test coverage gaps
Reference specific files and line numbers. If the PR looks
good, say so briefly rather than inventing nits.

# Defense-in-depth allowlist: read + comment only.
# Read: local files, ripgrep, and read-only GitHub API calls.
# Write surface is limited to posting PR/issue comments.
# No Edit/Write/Bash(git ...)/create_or_update tools are listed,
# so Claude cannot modify the tree even in-runner.
claude_args: |
--allowedTools "Read,Grep,Glob,mcp__github__get_pull_request,mcp__github__get_pull_request_files,mcp__github__get_pull_request_diff,mcp__github__list_pull_requests,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_file_contents,mcp__github__search_code,mcp__github_comment__update_claude_comment"
--max-turns 20
Loading