Skip to content

Harden Claude workflow permission gate#776

Merged
justin808 merged 1 commit into
masterfrom
codex/harden-claude-permission-gate
Jul 11, 2026
Merged

Harden Claude workflow permission gate#776
justin808 merged 1 commit into
masterfrom
codex/harden-claude-permission-gate

Conversation

@justin808

@justin808 justin808 commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

  • add a workflow-level authorize_claude_actor permission gate before Claude Code runs
  • require both the workflow actor and original Claude requester to have write/admin repository permission
  • keep the existing Claude trigger condition on the authorization job and only run the secret-bearing Claude job when authorized

Changed workflows

  • .github/workflows/claude.yml

Validation

  • actionlint passed for the changed workflow file(s)

Rollout context

This mirrors the verified gate from shakacode/react_on_rails#4533.
Positive path was exercised on react_on_rails#4534; negative read-only actor attempts on react_on_rails#4540 blocked before the secret-bearing Claude job.

Summary by CodeRabbit

  • Security
    • Added authorization checks before automated workflow actions run.
    • Actions now proceed only when both the triggering actor and request originator have sufficient repository permissions.
    • Unauthorized or unverifiable requests are skipped.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The Claude workflow now authorizes both the event actor and event requester through repository collaborator permissions before running the Claude job.

Changes

Claude authorization flow

Layer / File(s) Summary
Actor and requester permission gate
.github/workflows/claude.yml
Adds an authorization job that extracts the requester, checks admin or write permission for both participants, and exposes an authorized output.
Claude job authorization dependency
.github/workflows/claude.yml
Makes the Claude job depend on the authorization job and run only when authorization succeeds.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Event as GitHub event
  participant Authorization as authorize_claude_actor
  participant API as GitHub collaborator API
  participant Claude as claude job
  Event->>Authorization: provide actor and requester
  Authorization->>API: check actor permission
  Authorization->>API: check requester permission
  API-->>Authorization: return permission levels
  Authorization->>Claude: authorize when both checks pass
  Claude-->>Event: execute Claude workflow
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: strengthening the Claude workflow's permission gate.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/harden-claude-permission-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a permission gate before the Claude workflow runs. The main changes are:

  • A new authorize_claude_actor job before the Claude job.
  • Write/admin checks for both the workflow actor and extracted Claude requester.
  • A downstream Claude job that only runs when the gate outputs authorized == true.

Confidence Score: 4/5

The issues: assigned authorization path needs a fix before merging.

  • The job split and output gate preserve the intended fail-closed behavior.
  • The assigned-issue path can run Claude on mutable issue content supplied by a user who was not checked.
  • Comment and review-created paths use the content author directly.

.github/workflows/claude.yml

Security Review

The new gate reduces exposure, but the issues: assigned path can authorize the static issue author instead of the user who supplied the mutable @claude text in the issue title/body.

Important Files Changed

Filename Overview
.github/workflows/claude.yml Adds the Claude authorization job and downstream gate, with a remaining requester mismatch on assigned issue events.

Reviews (1): Last reviewed commit: "Harden Claude workflow permission gate" | Re-trigger Greptile

Comment on lines +47 to +49
if (context.eventName === 'issues') {
return context.payload.issue?.user?.login;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Mutable Issue Body Authorized Wrongly

When an issues: assigned event fires, this returns the original issue author while the gate checks the current issue title/body for @claude. If a read-only user can edit an issue that was opened by a write/admin user, then a later assignment by a write/admin user can pass both permission checks and run Claude on instructions supplied by the read-only editor.

steps:
- name: Verify actor can run Claude
id: permission-gate
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to pin actions/github-script to a full commit SHA rather than a mutable tag — good supply-chain hygiene. Worth double-checking (outside this sandbox, which blocked outbound gh api calls to other repos) that 3a2844b7e9c422d3c10d287c895573f7108da1b3 is actually the commit tagged v9 upstream, since a stale/incorrect comment here would be misleading if this pin is ever rotated by hand later.

Comment on lines +91 to +103
if (!(await hasWriteAccessFor(actor))) {
core.setOutput('authorized', 'false');
core.setFailed(`Unable to run Claude because ${actor} does not have write/admin repository permission.`);
return;
}

if (!(await hasWriteAccessFor(requester))) {
core.setOutput('authorized', 'false');
core.setFailed(
`Unable to run Claude because requester ${requester} does not have write/admin repository permission.`
);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core.setFailed marks authorize_claude_actor (and thus the overall workflow run) as failed, not skipped, for every unauthorized @claude mention. On a public repo like this one, any external/non-collaborator who tags @claude in an issue/PR comment will produce a red ❌ workflow run visible in the Actions tab and as a check — which can look like a broken CI job rather than "expected access-denied". If that's intentional (e.g. for auditability), fine, but consider whether core.warning + a clean skip (leaving the job "successful" with authorized=false) would give a less noisy signal for the common case of well-meaning outside contributors.

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review: Harden Claude workflow permission gate

Overview

Splits the single claude job into two: a new authorize_claude_actor job that checks (via getCollaboratorPermissionLevel) that both the workflow actor and the original @claude requester have write/admin access, and the existing claude job now gated behind needs.authorize_claude_actor.outputs.authorized == 'true'. The secret-bearing job (CLAUDE_CODE_OAUTH_TOKEN) never runs unless both checks pass.

Code quality

  • Sound design: checking both actor and original requester closes a real gap — e.g. a maintainer assigning/re-triggering on an issue whose body was authored by a non-collaborator (potential prompt-injection vector) is now blocked, not just the immediate triggering actor.
  • Fail-safe defaults: any error from the permission-level API call is caught and treated as "no write access" (hasWriteAccess = false), so API/network hiccups deny rather than silently allow.
  • Requester-resolution cache (permissionByLogin Map) avoids duplicate API calls when actor == requester (the common case).
  • contents: read is the minimal permission granted to the new job, and the secret token is scoped only to the downstream claude job — good separation.
  • The job dependency (needs + explicit if) correctly cascades: GitHub Actions implicitly ANDs a job's custom if with success(), so a failed/skipped authorize_claude_actor run prevents claude from running even without the explicit check, and the explicit check makes the gate unambiguous.

Suggestions (left as inline comments)

  1. Action pin verificationactions/github-script is pinned to a commit SHA with a # v9 comment, which is best practice, but I couldn't verify from this sandbox (outbound gh api calls to other repos were blocked) that the SHA is actually the real v9 tag commit. Worth a manual double-check.
  2. Failure noisecore.setFailed on an unauthorized request marks the whole workflow run as failed (red ❌), not skipped. For a public repo, any external contributor tagging @claude without write access will produce a failed CI run rather than a clean "not authorized" skip. This may be intentional (audit trail), but worth confirming it's the desired UX.

Risks / other notes

  • No functional/behavioral change to the claude job itself besides the new gate — low blast radius.
  • actionlint validation mentioned in the PR description, and the approach is stated to be already validated on a companion repo (react_on_rails#4533/#4534/#4540), which is reassuring given workflow files aren't easily unit-tested.
  • This is a config-only (YAML) change with no app code/test coverage impact.

Overall: solid, low-risk security hardening. The two notes above are minor/non-blocking.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/claude.yml (1)

85-106: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Authorization logic is sound; consider not failing the run on denial.

The dual actor+requester gate, fail-closed error handling, and SHA-pinned github-script all look good. One operational nit: calling core.setFailed on every unauthorized @claude mention marks the whole workflow run as failed (red ❌). On a high-traffic public repo, drive-by mentions from non-collaborators will each produce a failed run and notifications. Since the claude job is already gated on authorized == 'true', you can deny by setting the output to false and logging, without failing the job.

Note: getCollaboratorPermissionLevel returns none for GitHub App bots (they authenticate via installation tokens, not as collaborators), so any bot-authored @claude request will be denied here — expected for a human-review gate, but worth confirming no bot-initiated flow relies on it.

♻️ Optional: deny without failing the run
             if (!requester) {
               core.setOutput('authorized', 'false');
-              core.setFailed('Unable to run Claude because the requester could not be determined.');
+              core.warning('Skipping Claude because the requester could not be determined.');
               return;
             }

             if (!(await hasWriteAccessFor(actor))) {
               core.setOutput('authorized', 'false');
-              core.setFailed(`Unable to run Claude because ${actor} does not have write/admin repository permission.`);
+              core.warning(`Skipping Claude because ${actor} does not have write/admin repository permission.`);
               return;
             }

             if (!(await hasWriteAccessFor(requester))) {
               core.setOutput('authorized', 'false');
-              core.setFailed(
-                `Unable to run Claude because requester ${requester} does not have write/admin repository permission.`
-              );
+              core.warning(
+                `Skipping Claude because requester ${requester} does not have write/admin repository permission.`
+              );
               return;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/claude.yml around lines 85 - 106, Replace the
core.setFailed calls in the requester-missing and unauthorized actor/requester
branches with non-failing core.warning or core.info logs while preserving
authorized=false and early returns. Keep the existing dual authorization checks
and successful authorized output unchanged; confirm bot-initiated flows are not
expected to pass this human-review gate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/claude.yml:
- Around line 85-106: Replace the core.setFailed calls in the requester-missing
and unauthorized actor/requester branches with non-failing core.warning or
core.info logs while preserving authorized=false and early returns. Keep the
existing dual authorization checks and successful authorized output unchanged;
confirm bot-initiated flows are not expected to pass this human-review gate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dd5ad90b-7c44-438b-a165-607fdd425a53

📥 Commits

Reviewing files that changed from the base of the PR and between ed9c5d8 and b3b5bc0.

📒 Files selected for processing (1)
  • .github/workflows/claude.yml

@github-actions

Copy link
Copy Markdown

🚀 Quick Review App Commands

Welcome! Here are the commands you can use in this PR:
They require the repository to have cpflow review apps configured, including the CPLN_TOKEN_STAGING secret.

+review-app-deploy

Deploy your PR branch for testing.

+review-app-delete

Remove the review app when done.

+review-app-help

Show detailed instructions, environment setup, and configuration options.

Comment +review-app-help for full setup details.

@justin808
justin808 merged commit 7a9dada into master Jul 11, 2026
21 of 24 checks passed
@justin808
justin808 deleted the codex/harden-claude-permission-gate branch July 11, 2026 02:53
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown

✅ Review App Deleted

Review app for PR #776 is deleted

🎮 Control Plane Console
📋 View Workflow Logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant