-
Notifications
You must be signed in to change notification settings - Fork 2.1k
258 lines (227 loc) · 11.7 KB
/
claude.yml
File metadata and controls
258 lines (227 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
name: claude-review
# Triggered by @claude mention in PR comments.
# The action extracts text after "@claude" as the prompt.
#
# Security model:
# - Only write/admin users can trigger (enforced by the action)
# - Network sandbox: Squid proxy (L7 domain allowlist) + iptables (L3 egress block)
# - All dependencies pre-installed before lockdown; action skips its internal installs
#
# Why pre-install? The action internally runs setup-bun, bun install, and claude install.
# These use fetch() which ignores HTTP_PROXY and gets blocked by iptables.
# We do them beforehand and pass paths via inputs to skip those steps.
#
# Secrets:
# - CLAUDE_CODE_OAUTH_TOKEN: Anthropic API auth (from `claude setup-token`)
# - CLAUDE_ACCESS_TOKEN: PAT with 'repo' scope for cloning private repos (zama-marketplace, tech-spec)
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request:
types: [opened, synchronize]
permissions: {}
jobs:
claude-review:
name: claude-review/respond
if: |
contains(github.event.comment.body, '@claude') &&
(github.event.issue.pull_request || github.event_name == 'pull_request_review_comment')
runs-on: ubuntu-latest
permissions:
contents: read # Checkout repository code and read files
pull-requests: write # Post review comments and update PR status
issues: write # Respond to @claude mentions in issue comments
id-token: write # OIDC token for GitHub App token exchange
actions: read # Read workflow run context for action inputs
steps:
# ── Phase 1: Setup (full network) ──────────────────────────────────
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b
- name: Clone private repositories
run: |
gh repo clone zama-ai/zama-marketplace /tmp/zama-marketplace
gh repo clone zama-ai/tech-spec /tmp/tech-spec
env:
GH_TOKEN: ${{ secrets.CLAUDE_ACCESS_TOKEN }}
- name: Build custom system prompt
id: custom_prompt
run: |
if [[ "$EVENT_NAME" == "pull_request" ]] || [[ -n "$ISSUE_PR_URL" ]]; then
if [[ "$EVENT_NAME" == "pull_request" ]]; then
PR_TITLE="$PR_TITLE_INPUT"
PR_AUTHOR="$PR_AUTHOR_INPUT"
PR_HEAD="$PR_HEAD_INPUT"
PR_BASE="$PR_BASE_INPUT"
PR_STATE="$PR_STATE_INPUT"
PR_ADDITIONS="$PR_ADDITIONS_INPUT"
PR_DELETIONS="$PR_DELETIONS_INPUT"
PR_COMMITS="$PR_COMMITS_INPUT"
PR_FILES="$PR_FILES_INPUT"
else
PR_NUMBER="$ISSUE_NUMBER_INPUT"
PR_DATA=$(gh pr view "$PR_NUMBER" --json title,author,headRefName,baseRefName,state,additions,deletions,commits)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
PR_HEAD=$(echo "$PR_DATA" | jq -r '.headRefName')
PR_BASE=$(echo "$PR_DATA" | jq -r '.baseRefName')
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
PR_ADDITIONS=$(echo "$PR_DATA" | jq -r '.additions')
PR_DELETIONS=$(echo "$PR_DATA" | jq -r '.deletions')
PR_COMMITS=$(echo "$PR_DATA" | jq -r '.commits | length')
PR_FILES=$(gh pr view "$PR_NUMBER" --json files | jq '.files | length')
fi
FORMATTED_CONTEXT="PR Title: ${PR_TITLE}
PR Author: ${PR_AUTHOR}
PR Branch: ${PR_HEAD} -> ${PR_BASE}
PR State: ${PR_STATE^^}
PR Additions: ${PR_ADDITIONS}
PR Deletions: ${PR_DELETIONS}
Total Commits: ${PR_COMMITS}
Changed Files: ${PR_FILES} files"
else
FORMATTED_CONTEXT="Issue Title: ${ISSUE_TITLE_INPUT}
Issue Author: ${ISSUE_AUTHOR_INPUT}
Issue State: ${ISSUE_STATE_INPUT^^}"
fi
SYSTEM_PROMPT="You are Claude, an AI assistant designed to help with GitHub issues and pull requests. Think carefully as you analyze the context and respond appropriately. Here's the context for your current task:
<formatted_context>
${FORMATTED_CONTEXT}
</formatted_context>"
{
echo "CUSTOM_SYSTEM_PROMPT<<EOF"
echo "$SYSTEM_PROMPT"
echo "EOF"
} >> "$GITHUB_ENV"
env:
GH_TOKEN: ${{ secrets.CLAUDE_ACCESS_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
ISSUE_PR_URL: ${{ github.event.issue.pull_request.url || '' }}
PR_TITLE_INPUT: ${{ github.event.pull_request.title }}
PR_AUTHOR_INPUT: ${{ github.event.pull_request.user.login }}
PR_HEAD_INPUT: ${{ github.event.pull_request.head.ref }}
PR_BASE_INPUT: ${{ github.event.pull_request.base.ref }}
PR_STATE_INPUT: ${{ github.event.pull_request.state }}
PR_ADDITIONS_INPUT: ${{ github.event.pull_request.additions }}
PR_DELETIONS_INPUT: ${{ github.event.pull_request.deletions }}
PR_COMMITS_INPUT: ${{ github.event.pull_request.commits }}
PR_FILES_INPUT: ${{ github.event.pull_request.changed_files }}
ISSUE_NUMBER_INPUT: ${{ github.event.issue.number }}
ISSUE_TITLE_INPUT: ${{ github.event.issue.title }}
ISSUE_AUTHOR_INPUT: ${{ github.event.issue.user.login }}
ISSUE_STATE_INPUT: ${{ github.event.issue.state }}
# ── Phase 2: Pre-install dependencies (before lockdown) ────────────
# The action's internal setup-bun, bun install, and claude install all
# use fetch() which ignores HTTP_PROXY → blocked by iptables.
# Pre-installing and passing paths via inputs skips those steps entirely.
# OIDC → Anthropic exchange → GitHub App token (normally done inside the action)
- name: Exchange OIDC for GitHub App token
id: oidc-exchange
run: |
OIDC_TOKEN=$(curl -sf \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=claude-code-github-action" | jq -r '.value')
if [ -z "$OIDC_TOKEN" ] || [ "$OIDC_TOKEN" = "null" ]; then
echo "::error::OIDC token request failed"; exit 1
fi
APP_TOKEN=$(curl -sf -X POST \
-H "Authorization: Bearer $OIDC_TOKEN" \
-H "Content-Type: application/json" \
-d '{"permissions":{"contents":"write","pull_requests":"write","issues":"write"}}' \
"https://api.anthropic.com/api/github/github-app-token-exchange" | jq -r '.token')
if [ -z "$APP_TOKEN" ] || [ "$APP_TOKEN" = "null" ]; then
echo "::error::Token exchange failed"; exit 1
fi
echo "::add-mask::$APP_TOKEN"
echo "app_token=$APP_TOKEN" >> "$GITHUB_OUTPUT"
# Bun runtime — needed by the action's TypeScript orchestrator
- name: Install Bun
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3
with:
bun-version: latest
# Action's node_modules — `bun install` inside the action would need network
- name: Pre-install action dependencies
id: setup-deps
run: |
cd "/home/runner/work/_actions/anthropics/claude-code-action/b433f16b30d54063fd3bab6b12f46f3da00e41b6"
bun install --production
echo "bun_path=$(which bun)" >> "$GITHUB_OUTPUT"
# Claude Code CLI via npm — native binary ignores HTTP_PROXY (anthropics/claude-code#14165)
- name: Install Claude Code (npm)
id: setup-claude
run: |
npm install -g @anthropic-ai/claude-code@2.1.42
echo "path=$(which claude)" >> "$GITHUB_OUTPUT"
# ── Phase 3: Network sandbox ───────────────────────────────────────
- name: Start Squid proxy
run: |
docker run -d --name sandbox-proxy -p 3128:3128 \
-v "${{ github.workspace }}/.github/squid/sandbox-proxy-rules.conf:/etc/squid/conf.d/00-sandbox-proxy-rules.conf:ro" \
ubuntu/squid
# Wait for readiness (api.github.com returns 200 without auth, unlike api.anthropic.com)
for i in $(seq 1 30); do
curl -sf -x http://localhost:3128 -o /dev/null https://api.github.com 2>/dev/null && break
[ "$i" -eq 30 ] && { echo "::error::Squid proxy failed to start"; docker logs sandbox-proxy; exit 1; }
sleep 2
done
# Verify: allowed domain works, blocked domain is rejected
HTTP_CODE=$(curl -s -x http://localhost:3128 -o /dev/null -w '%{http_code}' https://api.github.com)
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 400 ]; then
echo "::error::Allowed domain returned $HTTP_CODE"; exit 1
fi
if curl -sf -x http://localhost:3128 -o /dev/null https://google.com 2>/dev/null; then
echo "::error::Blocked domain reachable!"; exit 1
fi
- name: Lock down iptables
run: |
RUNNER_UID=$(id -u)
# Allow established connections, loopback, and Docker bridge
sudo iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A OUTPUT -o lo -j ACCEPT
sudo iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
# Block new outbound TCP from runner UID — forces proxy use
sudo iptables -A OUTPUT -m owner --uid-owner "$RUNNER_UID" -p tcp --syn -j REJECT --reject-with tcp-reset
# Verify: direct blocked, proxy works
if curl -sf --max-time 5 -o /dev/null https://google.com 2>/dev/null; then
echo "::error::Direct connection not blocked!"; exit 1
fi
if ! curl -sf --max-time 10 -x http://localhost:3128 -o /dev/null https://api.github.com 2>/dev/null; then
echo "::error::Proxy broken!"; exit 1
fi
# ── Phase 4: Run Claude Code (sandboxed) ───────────────────────────
- name: Run Claude Code
id: run-claude
uses: anthropics/claude-code-action@b433f16b30d54063fd3bab6b12f46f3da00e41b6 # 2026-02-10
env:
HTTP_PROXY: http://localhost:3128
HTTPS_PROXY: http://localhost:3128
NO_PROXY: localhost,127.0.0.1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.oidc-exchange.outputs.app_token }}
path_to_bun_executable: ${{ steps.setup-deps.outputs.bun_path }}
path_to_claude_code_executable: ${{ steps.setup-claude.outputs.path }}
plugin_marketplaces: "/tmp/zama-marketplace"
plugins: |
project-manager@zama-marketplace
zama-developer@zama-marketplace
prompt: ""
claude_args: |
--model opus
--dangerously-skip-permissions
--system-prompt "${{ env.CUSTOM_SYSTEM_PROMPT }}"
# ── Cleanup ────────────────────────────────────────────────────────
# The action skips token revocation when github_token is provided — do it ourselves
- name: Revoke GitHub App token
if: always() && steps.oidc-exchange.outputs.app_token != ''
run: |
curl -sf -X DELETE \
-H "Authorization: Bearer $APP_TOKEN" \
"https://api.github.com/installation/token"
env:
APP_TOKEN: ${{ steps.oidc-exchange.outputs.app_token }}