GitHub Actions "GITHUB_TOKEN does not have write access" (fork)
On pull_request runs from a fork, GITHUB_TOKEN is read-only by design to protect the base repository from untrusted code. Write operations (push, comment, label) are denied. This is a security boundary, not a misconfiguration to retry.
What this error means
A workflow triggered by a fork pull_request fails any write action with a permission error, while the same workflow works on same-repo branches.
Error: Resource not accessible by integration
GITHUB_TOKEN does not have write access to the repository (forked pull request).Diagnose it: what token do you actually have?
Permission failures in Actions are almost never about your repository settings alone. Three things combine: the default GITHUB_TOKEN permission set for the repo or organization, the permissions: block in the workflow, and whether the event is a fork pull request, which downgrades the token to read-only regardless of everything else.
- name: Show the token scopes actually granted
run: |
curl -sI -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/ | grep -i "^x-oauth-scopes\|^x-accepted"
echo "event: ${{ github.event_name }}"
echo "fork PR: ${{ github.event.pull_request.head.repo.fork }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Common causes
Fork PR token is read-only
pull_request from a fork downgrades GITHUB_TOKEN to read-only so untrusted code cannot mutate the base repo.
Write step running on the fork event
A commenting/labeling/push step is placed in a job that runs on the fork-PR trigger.
How to fix it
Move write work to a trusted trigger
- Use pull_request_target (with great care, no untrusted checkout of PR code) for trusted write operations on fork PRs.
- Or split: run untrusted build on pull_request, and post results from a workflow_run-triggered job with default permissions.
- Never expose secrets to untrusted fork code.
on:
workflow_run:
workflows: ["CI"]
types: [completed]
permissions:
pull-requests: writeGrant the narrowest permission that works
Declaring a permissions: block switches the job from the repository default to exactly what you list, so an incomplete block is a common cause of a new failure right after someone tightened security. List every scope the job needs, not just the one that failed.
permissions:
contents: read # checkout
packages: write # push to GHCR
id-token: write # OIDC to a cloud provider
pull-requests: write # comment on or label a PR
checks: write # publish check runsHow to prevent it
- Keep write operations off the untrusted fork pull_request trigger.
- Use workflow_run or carefully scoped pull_request_target for trusted post-processing.