GitHub Actions can fetch a pull request, send selected code to a model, and post review comments. The model call is the easy part. The hard part is keeping untrusted pull-request code away from secrets and write tokens.
Design the trust boundary before writing YAML. Decide how forked pull requests work, which job can read the model key, which token permissions are needed, and whether the workflow ever executes code from the pull request.
GitHub's secure use reference warns about privileged workflows that check out untrusted pull-request code. Keep it open while you design the workflow.
Choose where the review runs
There are three common designs.
A GitHub App or hosted review service
GitHub sends pull-request events to a service. The service uses its installation access to fetch repository data and post feedback.
This works well across many repositories because credentials, webhooks, queues, retries, and context retrieval live in one place. The workflow does not need a model key in every repository.
A self-hosted review service triggered by Actions
The workflow sends a pull-request number or event to an internal service. That service fetches the selected data, calls the model, and posts the review.
This keeps the review logic out of workflow YAML and lets the team control the application and model path. It also means the team owns deployment, credentials, monitoring, and upgrades.
A review script inside Actions
The workflow fetches context, calls the model, and publishes feedback on the runner. This is easy to try in one repository.
It is also the design most likely to mix contributor-controlled code with secrets. Use it only when the trust boundary is explicit and the job never gives untrusted code access to sensitive credentials.
Understand pull_request before adding secrets
For pull requests from forks, GitHub normally gives a pull_request workflow a read-only GITHUB_TOKEN and does not provide repository secrets. The workflow syntax documentation describes the permission behavior.
That protects the repository, but it creates two practical limits for AI review:
- the job cannot read a model key stored as a repository secret
- the job cannot usually post a review with
pull-requests: write
A workflow may work for a branch from a trusted repository and fail for a public fork. Do not solve that by sending secrets to every fork workflow. A contributor can change dependencies, scripts, workflow inputs, and other executable files.
Treat pull_request_target as privileged
pull_request_target runs with the base repository context. It can access secrets and a writable token when the workflow permissions allow it. That makes it useful for trusted metadata work on forked pull requests.
It also makes this pattern dangerous:
on: pull_request_target
steps:
- uses: actions/checkout@<commit-sha>
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install
- run: npm testThe job has checked out contributor-controlled code and run its scripts in a privileged context. An install script, test command, build tool, or changed action can attempt to read secrets or use the token.
GitHub recommends avoiding privileged triggers with untrusted code. If a trusted workflow needs to review a fork, fetch the diff and selected files through the GitHub API as data. Do not check out or execute the contributor's branch. The script injection guidance covers the same rule for pull-request text and other GitHub context values.
A safer workflow for trusted branches
If every author has permission to access the repository secrets, a simple pull_request workflow can work:
name: AI code review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
permissions:
contents: read
pull-requests: write
concurrency:
group: ai-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out the reviewed commit
uses: actions/checkout@<full-commit-sha>
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Run the review client
env:
GITHUB_TOKEN: ${{ github.token }}
MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }}
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
REVIEWED_SHA: ${{ github.event.pull_request.head.sha }}
run: ./scripts/run-ai-reviewThis is a skeleton. The review client must come from the trusted base branch or a separately reviewed and pinned action. If the pull request can replace the client before the step runs, the model key is exposed.
For public forks, use a trusted script from the base branch and fetch pull-request data through the API:
- Run trusted review code from the base branch.
- Fetch the diff and selected files through GitHub's API.
- Treat file contents, filenames, commit messages, titles, and descriptions as untrusted strings.
- Send those strings to the model as data.
- Validate the model response before posting comments.
If public-contributor code cannot be sent to your model provider, skip fork reviews or route them through a provider and account approved for that data.
Keep untrusted text out of shell commands
Pull-request titles, branch names, labels, descriptions, file names, and model output can contain shell metacharacters. Do not interpolate them into a run command:
- run: echo "${{ github.event.pull_request.title }}"Pass the value through an environment variable instead:
- name: Record the pull-request title
env:
PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }}
run: printf '%s\n' "$PULL_REQUEST_TITLE"The same rule applies to JSON passed to another command. Parse it as data. Do not execute commands suggested by the model.
Give the token the smallest permission
A reviewer that reads repository content and posts a pull-request review generally needs:
permissions:
contents: read
pull-requests: writeIf the job only creates an artifact, use contents: read or permissions: {} when it does not need the token. Do not grant contents: write, actions: write, workflows: write, or repository administration access to a reviewer.
Declare permissions at the workflow or job level. GitHub can still provide github.token to actions even if you do not pass it as an input, so inspect every action that runs in the job.
Protect the model key
A model key can incur a bill even when it cannot write to GitHub. Treat it like a production credential:
- store it in GitHub secrets or an approved secret manager
- use a separate key from developer workstations and production applications
- apply provider budgets and rate limits
- avoid logging provider errors that may contain request data
- rotate the key after suspicious workflow activity
- keep it out of forked
pull_requestjobs - require environment approval when the model or repository is sensitive
BYOK AI code review changes who owns the model account. It does not remove the need for safe workflow permissions or a clear repository data path.
Pin actions and keep the client small
Every action in a privileged job can access the job's environment and token. Pin third-party actions to a full-length commit SHA, review the source, and update the pin through a controlled process.
Avoid installing a large dependency tree in the same job that holds a model key. Every install script and transitive package becomes part of the trusted code path.
Fetch enough context without sending the whole repository
A raw diff is often too small for a useful review. The workflow may need the changed functions, direct callers, types, schemas, tests, repository rules, and pull-request description.
It rarely needs every file. A bounded retrieval process can:
- list changed files and classify generated or dependency noise
- fetch changed hunks with stable line information
- open complete changed functions or classes
- retrieve direct callers, definitions, and relevant tests
- include repository review rules and the pull-request description
- cap total files, bytes, and model tokens
- record which evidence supports each finding
The examples in why diff-only code review misses bugs show why surrounding code matters. The limits matter too. An enormous pull request can consume runner time and model budget.
Publish one bounded review
Do not post every candidate finding. Generate candidates, verify them, remove unsupported claims, and publish a small number of useful comments.
Before posting, check that:
- the reviewed commit still matches the pull-request head
- every inline comment points to an accepted changed line
- the finding describes a concrete failure rather than a style preference
- severity comes from a small documented set
- the body and comment count stay within configured limits
- earlier duplicate findings are updated or suppressed
If the head commit changes during analysis, discard the result and let the newer run replace it. Stale comments waste the author's time and can point to code that no longer exists.
Control retries, concurrency, and cost
The synchronize event can fire several times while an author pushes fixes. Use a concurrency group keyed by pull-request number and cancel older runs.
Set limits for:
- job and model-call time
- changed files and bytes
- model calls per review
- output comments
- repository or organization budgets
- retries and provider rate limits
Do not retry a failed review forever. A provider outage should produce one clear status, not a queue of duplicate reviews that arrive after the pull request merges.
Keep deterministic checks separate
An AI reviewer should not guess whether the project compiles or whether a linter failed. Run separate jobs for:
- compilation and type checking
- unit and integration tests
- formatting and linting
- static analysis
- dependency and secret scanning
- generated-file verification
Feed their results into the review only when that adds context. A failed test is already a useful signal. Asking a model to paraphrase it can make the pull request noisier.
Measure whether the workflow helps
Track the outcomes by repository:
- reviews started, completed, cancelled, and timed out
- queue time and model latency
- model and Actions cost
- findings accepted, dismissed, and duplicated
- bugs missed even though the evidence was available
- comments posted on stale commits
- forked pull requests skipped because secrets were unavailable
- reviewer time spent investigating feedback
A green workflow run proves that the automation executed. It does not prove that the review helped.
Frequently asked questions
Can GitHub Actions review a pull request with AI?
Yes. A workflow can fetch pull-request data, call a model, and submit review comments through the GitHub API. You still need to design context retrieval, finding verification, secret handling, and fork behavior.
Why does an AI review workflow fail on forked pull requests?
GitHub normally withholds repository secrets and gives forked pull_request workflows a read-only token. That protects the repository. Use a trusted service, skip forks, or review fork data without executing the branch.
Is pull_request_target safe for AI review?
It can be safe when trusted base-branch code fetches the pull-request diff as data. It becomes dangerous when the privileged job checks out and runs code from the pull-request branch.
What permissions does an AI review workflow need?
Reading content and posting review feedback usually requires contents: read and pull-requests: write. Grant less when the workflow does not publish comments.
Should an AI workflow approve pull requests automatically?
No. Submit advisory comments or a documented status. Human reviewers should own product intent, architecture, and accepted risk.
Should I build this in Actions or use a GitHub App?
Actions works for repository-local experiments and controlled workflows. A GitHub App or review service is usually easier to operate across many repositories because it centralizes credentials, webhooks, queues, and context retrieval.
Start with one repository and one trusted review path. Test it on historical pull requests, inspect every permission and log, and expand only after the comments prove useful.
For a provider-neutral version of the design, read how to add AI code review to CI/CD without blocking merges. It covers trigger choice, bounded context, stale reviews and the difference between advisory feedback and a merge gate.