@@ -20,6 +20,8 @@ name: Claude Code (Review)
2020# claude-write.yml so maintainers can ask Claude to make follow-up changes there.
2121
2222concurrency :
23+ # `issue_comment` events on PRs expose the PR number via `github.event.issue.number`,
24+ # so this falls back there when `github.event.pull_request.number` is unset.
2325 group : ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
2426 cancel-in-progress : true
2527
8890 pullNumber = context.payload.pull_request?.number ?? null;
8991 }
9092
93+ if (pullNumber) {
94+ core.setOutput('pull_number', String(pullNumber));
95+ }
96+
9197 let reason = '';
9298
9399 if (!mentioned) {
@@ -103,11 +109,15 @@ jobs:
103109 const permission = await getPermission(sender);
104110 core.setOutput('actor_permission', permission);
105111 actorHasWrite = ['admin', 'maintain', 'write'].includes(permission) ? 'true' : 'false';
106- if (actorHasWrite != 'true') {
112+ if (actorHasWrite !== 'true') {
107113 reason = 'actor_lacks_write';
108114 }
109115 }
110116
117+ if (!reason && context.eventName === 'issue_comment' && !trustedClaudeLogin) {
118+ reason = 'missing_claude_app_login';
119+ }
120+
111121 if (!reason) {
112122 let pr = null;
113123 const response = await github.rest.pulls.get({
@@ -118,8 +128,7 @@ jobs:
118128 pr = response.data;
119129
120130 const headRepo = pr.head.repo;
121- const isFork = !headRepo || Boolean(headRepo.fork) || headRepo.full_name !== `${context.repo.owner}/${context.repo.repo}`;
122- core.setOutput('pull_number', String(pullNumber));
131+ const isFork = !headRepo || headRepo.fork || headRepo.full_name !== `${context.repo.owner}/${context.repo.repo}`;
123132 core.setOutput('checkout_ref', pr.head.sha);
124133
125134 if (isFork) {
@@ -139,6 +148,8 @@ jobs:
139148 pull_number: pullNumber,
140149 per_page: 100,
141150 });
151+ // Refuse PRs that modify `.github/` because workflow files define the
152+ // automation policy this review job is enforcing.
142153 if (files.some(f => f.filename.startsWith('.github/'))) {
143154 reason = 'modifies_github_dir';
144155 }
@@ -151,24 +162,28 @@ jobs:
151162 env :
152163 CLAUDE_APP_LOGIN : ${{ vars.CLAUDE_APP_LOGIN }}
153164
154- refuse-fork :
155- name : Explain Fork Refusal
165+ explain-refusal :
166+ name : Explain Refusal
156167 needs : gate
157- if : needs.gate.outputs.reason == 'fork_pr_refused' && needs.gate.outputs.actor_has_write == 'true'
168+ if : |
169+ needs.gate.outputs.actor_has_write == 'true' &&
170+ (
171+ needs.gate.outputs.reason == 'fork_pr_refused' ||
172+ needs.gate.outputs.reason == 'modifies_github_dir' ||
173+ needs.gate.outputs.reason == 'missing_claude_app_login'
174+ )
158175 runs-on : ubuntu-latest
159176 permissions :
160177 issues : write
161178 pull-requests : write
162179 steps :
163- - name : Comment with the fork policy
180+ - name : Comment with the refusal reason
164181 uses : actions/github-script@v7
165182 with :
166183 script : |
167- await github.rest.issues.createComment({
168- owner: context.repo.owner,
169- repo: context.repo.repo,
170- issue_number: Number(${{ needs.gate.outputs.pull_number }}),
171- body: [
184+ const reason = ${{ toJSON(needs.gate.outputs.reason) }};
185+ const messages = {
186+ fork_pr_refused: [
172187 "Claude review automation is disabled for fork pull requests.",
173188 "",
174189 "Why:",
@@ -177,7 +192,32 @@ jobs:
177192 "- there is no promotion path for forks",
178193 "",
179194 "If maintainers want Claude to implement a change, restate the task on an issue and use the issue-driven Claude workflow instead."
180- ].join("\\n"),
195+ ].join("\n"),
196+ modifies_github_dir: [
197+ "Claude review automation is disabled for pull requests that modify `.github/` files.",
198+ "",
199+ "Why:",
200+ "- workflow and action files are part of the automation policy",
201+ "- this review workflow refuses to evaluate automation changes from the same PR",
202+ "",
203+ "Ask a human reviewer to inspect workflow changes directly."
204+ ].join("\n"),
205+ missing_claude_app_login: [
206+ "Claude review automation is misconfigured for issue-comment triggers.",
207+ "",
208+ "Why:",
209+ "- `CLAUDE_APP_LOGIN` is not set",
210+ "- without that bot login, the review workflow cannot safely route comments on Claude-owned PRs to the write workflow",
211+ "",
212+ "Set the `CLAUDE_APP_LOGIN` Actions variable, then retry the command."
213+ ].join("\n"),
214+ };
215+
216+ await github.rest.issues.createComment({
217+ owner: context.repo.owner,
218+ repo: context.repo.repo,
219+ issue_number: Number(${{ needs.gate.outputs.pull_number }}),
220+ body: messages[reason],
181221 });
182222
183223 claude :
0 commit comments