Skip to content

Commit f95a287

Browse files
Merge pull request #17 from smartwatermelon/claude/harden-review-cost-guards-20260320
feat(blocking-review): add cost guards and scope constraints
2 parents 3a8c328 + a22b434 commit f95a287

2 files changed

Lines changed: 82 additions & 37 deletions

File tree

.github/workflows/claude-blocking-review.yml

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ on:
3333
type: string
3434
required: false
3535
default: ''
36+
max_turns:
37+
description: 'Maximum Claude API turns (prevents runaway exploration). Default: 6.'
38+
type: number
39+
required: false
40+
default: 6
41+
timeout_minutes:
42+
description: 'Hard timeout for the Claude review step in minutes. Default: 4.'
43+
type: number
44+
required: false
45+
default: 4
46+
model:
47+
description: 'Claude model ID. Default: claude-sonnet-4-6.'
48+
type: string
49+
required: false
50+
default: 'claude-sonnet-4-6'
3651
secrets:
3752
claude_oauth_token:
3853
description: 'Claude Code OAuth token (CLAUDE_CODE_OAUTH_TOKEN secret)'
@@ -53,8 +68,39 @@ jobs:
5368
with:
5469
fetch-depth: 1
5570

71+
- name: Validate inputs
72+
env:
73+
MODEL: ${{ inputs.model }}
74+
MAX_TURNS: ${{ inputs.max_turns }}
75+
TIMEOUT: ${{ inputs.timeout_minutes }}
76+
run: |
77+
# Validate model: alphanumeric, dots, hyphens only
78+
if ! echo "$MODEL" | grep -qE '^[a-zA-Z0-9._-]+$'; then
79+
echo "::error::Invalid model name. Must match [a-zA-Z0-9._-]+"
80+
exit 1
81+
fi
82+
# Validate max_turns: positive integer, capped at 20
83+
if ! echo "$MAX_TURNS" | grep -qE '^[0-9]+$'; then
84+
echo "::error::max_turns must be a positive integer"
85+
exit 1
86+
fi
87+
if [ "$MAX_TURNS" -lt 1 ] || [ "$MAX_TURNS" -gt 20 ]; then
88+
echo "::error::max_turns must be between 1 and 20"
89+
exit 1
90+
fi
91+
# Validate timeout_minutes: between 1 and 15
92+
if ! echo "$TIMEOUT" | grep -qE '^[0-9]+$'; then
93+
echo "::error::timeout_minutes must be a positive integer"
94+
exit 1
95+
fi
96+
if [ "$TIMEOUT" -lt 1 ] || [ "$TIMEOUT" -gt 15 ]; then
97+
echo "::error::timeout_minutes must be between 1 and 15"
98+
exit 1
99+
fi
100+
56101
- name: Run Claude Code Review
57102
id: claude-review
103+
timeout-minutes: ${{ inputs.timeout_minutes }}
58104
continue-on-error: true # infrastructure failure must not block merges
59105
uses: anthropics/claude-code-action@v1
60106
with:
@@ -63,6 +109,13 @@ jobs:
63109
REPO: ${{ github.repository }}
64110
PR NUMBER: ${{ inputs.pr_number }}
65111
112+
SCOPE CONSTRAINTS — follow these strictly:
113+
- Read the PR diff using `gh pr diff`
114+
- Read changed files for immediate context around modified lines
115+
- Do NOT explore the broader codebase, run tests, or investigate unrelated files
116+
- Focus your review on the diff — do not review unchanged code
117+
- Complete your review in as few steps as possible
118+
66119
Please review this pull request and provide feedback on:
67120
- Code quality and best practices
68121
- Potential bugs or logic errors
@@ -145,7 +198,7 @@ jobs:
145198
The verdict file (Step 4) is the primary signal read by CI. The verdict line
146199
appended to the comment (Step 2) is the fallback. Both must match.
147200
148-
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(echo *),Bash(cat *),Bash(tee *)"'
201+
claude_args: '--max-turns ${{ inputs.max_turns }} --model ${{ inputs.model }} --allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(echo *),Bash(cat *),Bash(tee *)"'
149202

150203
- name: Check review verdict
151204
if: always()
Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,36 @@
11
name: Claude Code Review
22

3+
# Example caller workflow for claude-blocking-review.yml.
4+
# Copy this file into your repository's .github/workflows/ directory,
5+
# configure the secret, and update the org/owner below if needed.
6+
#
7+
# NOTE: The 'uses:' reference below points to smartwatermelon/github-workflows
8+
# which is the canonical source. Update the org name if you forked this repo.
9+
#
10+
# Prerequisites:
11+
# 1. CLAUDE_CODE_OAUTH_TOKEN secret available (repo or org level)
12+
# 2. (Optional) Branch protection rule requiring status check:
13+
# "Claude Code Review / claude-review / run-review"
14+
15+
permissions:
16+
contents: read
17+
pull-requests: write
18+
issues: write
19+
id-token: write
20+
321
on:
422
pull_request:
523
types: [opened, synchronize, ready_for_review, reopened]
6-
# Optional: Only run on specific file changes
7-
# paths:
8-
# - "src/**/*.ts"
9-
# - "src/**/*.tsx"
10-
# - "src/**/*.js"
11-
# - "src/**/*.jsx"
1224

1325
jobs:
1426
claude-review:
15-
# Optional: Filter by PR author
16-
# if: |
17-
# github.event.pull_request.user.login == 'external-contributor' ||
18-
# github.event.pull_request.user.login == 'new-developer' ||
19-
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20-
21-
runs-on: ubuntu-latest
22-
permissions:
23-
contents: read
24-
pull-requests: read
25-
issues: read
26-
id-token: write
27-
28-
steps:
29-
- name: Checkout repository
30-
uses: actions/checkout@v4
31-
with:
32-
fetch-depth: 1
33-
34-
- name: Run Claude Code Review
35-
id: claude-review
36-
uses: anthropics/claude-code-action@v1
37-
with:
38-
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39-
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
40-
plugins: 'code-review@claude-code-plugins'
41-
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
42-
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
43-
# or https://code.claude.com/docs/en/cli-reference for available options
44-
27+
uses: smartwatermelon/github-workflows/.github/workflows/claude-blocking-review.yml@v1
28+
with:
29+
pr_number: ${{ github.event.pull_request.number }}
30+
# extra_instructions: |
31+
# Repo-specific guidance here.
32+
# max_turns: 6 # default: 6
33+
# timeout_minutes: 4 # default: 4
34+
# model: claude-sonnet-4-6 # default: claude-sonnet-4-6
35+
secrets:
36+
claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

0 commit comments

Comments
 (0)