fix: allow triage-permission actors to trigger review via allow-list - #10
Conversation
The write-access preflight (and claude-code-action's own internal check) only accept the collaborator-permission API's admin/write levels. A fine-grained "triage" role rounds down to "read" via that API, so a triage-only bot/service account applying the trigger label always got rejected, even though it's a trusted account for this narrow purpose. Add an allowed_non_write_users input (comma-separated usernames, or "*") that bypasses the fast preflight by exact actor match and is threaded through to claude-code-action's own allowed_non_write_users input (which also needs github_token explicitly passed — previously only set via the GH_TOKEN env var — to actually honor the bypass). Also drop "maintain" from the preflight's accepted permissions: claude-code-action itself only accepts admin/write, so the preflight was silently letting maintain-level actors through to fail later with the exact opaque ~40s error it exists to avoid. Confirmed against dash-evo-tool run 29930647048: actor Claudius-Maginificent (triage role) reported permission "read" and was rejected before this fix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the composite action to support allowing specific trusted non-write actors (e.g., triage-permission bots) to trigger the review via an explicit allow-list, while aligning the preflight permission gate with what anthropics/claude-code-action actually enforces.
Changes:
- Adds a new
allowed_non_write_usersinput and documents its use in the README. - Extends the action’s fast “triggering actor permission” preflight to honor the allow-list and removes
maintainfrom the accepted permissions. - Passes
github_tokenandallowed_non_write_usersthrough toanthropics/claude-code-action@v1so the upstream bypass can activate.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| README.md | Documents the new allowed_non_write_users input and describes how triage-only actors can trigger reviews. |
| action.yml | Adds the new input, implements the allow-list bypass in the preflight, tightens accepted permissions, and forwards inputs to claude-code-action. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The write-access preflight's allowed_non_write_users match was both whitespace- and case-sensitive: a wildcard value with a stray space (e.g. "* ") wouldn't hit the exact "*" comparison, and GitHub usernames are case-insensitive so a differently-cased list entry (or actor login) would silently fail to match and reject a trusted actor. Trim the whole input before the wildcard check and lowercase both sides of the per-entry comparison. Verified with 7 standalone bash test cases covering the two failure modes flagged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
action.yml:239
- To keep the fast preflight and
claude-code-actionin sync, pass the same normalizedallowed_non_write_usersvalue to the underlying action (e.g. theALLOWED_NON_WRITE_USERSenv var exported by the preflight). Otherwise whitespace/casing normalization only applies to the preflight, and the upstream bypass may still fail unexpectedly.
github_token: ${{ inputs.github_token }}
allowed_non_write_users: ${{ inputs.allowed_non_write_users }}
…value Two related gaps from the previous whitespace/case fix, both flagged by Copilot review: - The preflight normalized allowed_non_write_users for its own matching, but the claude-code-action step still received the raw, unnormalized input — the two steps could disagree on edge cases (e.g. trailing whitespace in "* "), reintroducing the opaque ~40s failure this preflight exists to prevent. Fixed by exporting the trimmed value via GITHUB_ENV (ALLOWED_NON_WRITE_USERS) and having both steps read that single normalized value instead of computing it twice. - The case-insensitive matching added last commit was actually a regression: claude-code-action compares the actor string with exact case (no case-folding), so a differently-cased list entry would now pass this preflight but still get rejected 40s later inside claude-code-action itself — worse than before, since it converts a fast, clear failure into a slow, opaque one. Reverted preflight matching to trim-only, exact-case — now structurally identical to claude-code-action's own comparison, so the two can never disagree. Re-verified with the same 7 standalone bash test cases, updated to assert the case-mismatch scenario is now correctly rejected rather than falsely accepted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
action.yml:109
- The normalized
allowed_non_write_usersvalue is only exported toGITHUB_ENVafter theACTORempty guard. Whengithub.event.sender.loginis empty, this step exits early and the downstreamclaude-code-actionstep receives an empty${{ env.ALLOWED_NON_WRITE_USERS }}, even if the user configuredinputs.allowed_non_write_users. Export the normalized value before the early-exit so pass-through remains consistent when the preflight is skipped.
ACTOR="${{ github.event.sender.login }}"
if [ -z "$ACTOR" ]; then
echo "::warning::Could not determine the triggering actor (github.event.sender.login was empty) — skipping the write-access preflight. The underlying claude-code-action will still enforce this itself."
exit 0
fi
TL;DR: Lets a trusted bot/service account without repo write access (e.g. a triage-only labeling bot) trigger the review, instead of always being rejected.
User story
As a repo maintainer, I want to let a specific triage-permission bot trigger the AI review by applying the trigger label, to achieve automated review triggering without granting that bot real write access to the repository.
Scenario
Base flow
A bot or human applies the trigger label (e.g.
claudius-review) to a PR, which fires this action.Actual behavior
The action's own preflight — and
claude-code-actioninternally — only accept the GitHub collaborator-permission API'sadmin/writelevels for the triggering actor. A fine-grainedtriagerole rounds down toreadvia that API (it has no way to report "triage" directly), so any triage-only account is always rejected, even when it's a trusted account whose only job is applying the label. Confirmed ondashpay/dash-evo-toolrun29930647048: actorClaudius-Maginificent(triage role) reported permissionreadand was rejected.Expected behavior
A repo can opt specific trusted non-write accounts into triggering the review, without loosening write access for anyone else.
Detailed discussion
What was done
allowed_non_write_usersinput (comma-separated usernames, or*) that:claude-code-action's own bypass semantics).claude-code-action's ownallowed_non_write_usersinput, which also requiresgithub_tokento be explicitly provided (previously this action only setGH_TOKENas an env var for its ownghcalls, never passedgithub_tokenas an actual input to theclaude-code-actionstep) — without both, the upstream bypass silently never activates.maintainfrom the preflight's accepted permissions.claude-code-actionitself only acceptsadmin/write, so amaintain-level actor was previously passing this fast preflight only to fail ~40s later insideclaude-code-actionwith the exact opaque error this preflight exists to avoid.Testing
action.ymlandlearn/action.ymlvalidated withpython3 -c "import yaml; yaml.safe_load(...)"(pass) andyamllintviapipx run(no new issues beyond this file's pre-existing 80-col line-length style, unrelated to this change).set -e— no false matches, no spurious failures on empty/default input.CLAUDE.md); this action is exercised by consuming workflows once merged tomain.Breaking changes
None — new input defaults to
""(empty allow-list), identical behavior to before for anyone who doesn't set it. The only behavior change without opting in is themaintain→ rejected fix (a permission levelclaude-code-actionnever actually honored anyway).Checklist
dashpay/dash-evo-tool#TBDoptsClaudius-Maginificentintoallowed_non_write_usersonce this merges tomain)Prior work
Attribution
🤖 Co-authored by Claudius the Magnificent AI Agent