name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
claude-review:
if: |
!contains(fromJSON('["claude","claude[bot]","github-actions[bot]"]'), github.event.pull_request.user.login)
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- name: Check for Claude token
id: gate
env:
CLAUDE_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
run: |
if [ -n "$CLAUDE_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::CLAUDE_CODE_OAUTH_TOKEN is not set — skipping Claude review. Add it with 'gh secret set CLAUDE_CODE_OAUTH_TOKEN'."
fi
- name: Checkout repository
if: steps.gate.outputs.enabled == 'true'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with:
fetch-depth: 1
- name: Delete previous Claude review comment
if: steps.gate.outputs.enabled == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Remove any prior review comment (identified by the marker) so each
# push replaces the review instead of stacking comments.
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
--jq '.[] | select(.body | contains("<!-- claude-code-review -->")) | .id' \
| while read -r id; do
if [ -n "$id" ]; then
echo "Deleting previous Claude review comment: $id"
gh api "repos/${{ github.repository }}/issues/comments/$id" -X DELETE
fi
done
- name: Load review prompt from base branch
if: steps.gate.outputs.enabled == 'true'
id: prompt
run: |
# Load the prompt from the PR's BASE commit (trusted), not the PR head.
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}"
BASE="${{ github.event.pull_request.base.sha }}"
PROMPT_PATH=".github/prompts/code-review.md"
PROMPT_REF="$BASE"
if ! git cat-file -e "${BASE}:${PROMPT_PATH}"; then
# The repository bootstrap PR predates the prompt on main. Permit
# only the exact reviewed prompt; later PRs always use the base copy.
EXPECTED_SHA256="2f3f2cc67e26dabbc9752d879505b998c9f6131c69414492cf4cbb2fdf0b1297"
ACTUAL_SHA256=$(git show "HEAD:${PROMPT_PATH}" | sha256sum | awk '{print $1}')
if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then
echo "::error::Bootstrap review prompt hash mismatch"
exit 1
fi
PROMPT_REF="HEAD"
fi
DELIM="PROMPT_EOF_$(openssl rand -hex 16)"
{
printf 'content<<%s\n' "$DELIM"
git show "${PROMPT_REF}:${PROMPT_PATH}"
printf '%s\n' "$DELIM"
} >> "$GITHUB_OUTPUT"
- name: Run Claude Code Review
if: steps.gate.outputs.enabled == 'true'
uses: anthropics/claude-code-action@ff9acae5886d41a99ed4ec14b7dc147d55834722 with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
allowed_bots: "dependabot[bot],github-actions[bot],renovate[bot]"
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
${{ steps.prompt.outputs.content }}
claude_args: >-
--max-turns 30
--allowed-tools "Read,Glob,Grep,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*)"