GitHub Actions "Resource not accessible by integration": give the token the right permission
Fix GitHub Actions "Resource not accessible by integration" (HTTP 403): add the right permissions to GITHUB_TOKEN, and handle fork pull requests and Dependabot safely.
by OhSudo ··
Part of a learning path:Git for everyday work
A workflow step that talks to GitHub fails:
Error: Resource not accessible by integrationHttpError: Resource not accessible by integration status: 403The “integration” is your workflow, using its automatic GITHUB_TOKEN. The token works, and it was refused because it doesn’t have the permission this request needs: commenting on a pull request, adding a label, pushing, creating a release.
It often appears out of the blue on a workflow copied from elsewhere, since new repositories and organizations give that token read-only access by default.
- Workflow stepcomment, label, push, releaseuses
- GITHUB_TOKENread-only by defaultnot allowed(fails)
- GitHub API403 Resource not accessible(breaks here)
TL;DR
Add what the job needs, in the workflow:
jobs: comment: runs-on: ubuntu-latest permissions: contents: read pull-requests: writeIf it still fails on a pull request from a fork, that’s on purpose: see section 3.
1. Which permission?
Look at which step failed, and what it does:
| The step | Needs |
|---|---|
| Comments on or labels a pull request | pull-requests: write (some endpoints also issues: write) |
| Comments on or labels an issue | issues: write |
| Pushes commits or tags, creates a release | contents: write |
| Pushes to GitHub Container Registry | packages: write |
| Sets commit statuses | statuses: write |
| Publishes check runs (test reporters) | checks: write |
| Deploys to GitHub Pages | pages: write and id-token: write |
| Uses OIDC, for example to log in to a cloud | id-token: write |
| Uploads code scanning results | security-events: write |
If you’re not sure, GitHub tells you. On a REST API call, the response header x-accepted-github-permissions lists what the endpoint accepts. Look for it with curl -i or gh api -i.
2. Set it in the workflow
The permissions key can sit at the top, for all jobs, or better, on the job that needs it:
permissions: contents: read
jobs: release: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v7 - run: gh release create "$GITHUB_REF_NAME" --generate-notes env: GH_TOKEN: ${{ github.token }}Two details catch people out:
- Setting
permissionsturns everything you don’t list tonone. Addpull-requests: writealone, and a private repository’sactions/checkoutcan start failing, becausecontents: readis gone. - Avoid
permissions: write-all. It works, but a compromised action could then do anything the token can. Ask for the smallest set.
The CI/CD Pipeline Generator writes workflows this way, read-only by default, with packages: write only on the job that pushes an image.
3. Pull requests from forks
Here adding permissions won’t help. On a pull_request event from a fork, the token is always read-only, whatever the workflow says, and repository secrets aren’t available. That’s what stops a stranger’s pull request from changing your repository.
So how do you comment on those pull requests? Split the work in two:
- A
pull_requestworkflow runs the untrusted code, with no permissions, and uploads the result as an artifact. - A
workflow_runworkflow, which runs from your default branch with write access, reads that artifact and posts the comment.
Warning
You’ll see pull_request_target recommended, since it has a write token even for forks. It’s safe only if you never check out or run the pull request’s code in it. Doing so hands your token and secrets to whoever opened the pull request, and this has compromised real projects.
Dependabot has the same treatment: its pull_request runs get a read-only token, and its own set of secrets, under Settings → Secrets and variables → Dependabot. Its token can be raised with permissions on pull_request_target, or on a workflow triggered another way.
4. Repository and organization settings
Check Settings → Actions → General → Workflow permissions. “Read repository contents and packages permissions” is the default, and “Allow GitHub Actions to create and approve pull requests” is a separate box: without it, creating a pull request fails with a similar message, even with pull-requests: write.
An organization can lock these settings, in which case they’re greyed out for the repository, and an organization owner has to change them. permissions in a workflow can’t go above what they allow.
5. When GITHUB_TOKEN isn’t enough
The token only reaches its own repository. To open issues in another one, read a private repository or a private package elsewhere, use a GitHub App token (actions/create-github-app-token), or a fine-grained personal access token, stored as a secret:
- run: gh issue create --repo acme/other --title "Build failed" --body "See the run" env: GH_TOKEN: ${{ secrets.OTHER_REPO_TOKEN }}And an action might not use the token you think: check its token input.
Shell, we have a problem!
Similar, but different: Permission to acme/app.git denied to github-actions[bot] is the same problem during a git push: add contents: write. Resource not accessible by personal access token is the same refusal for a fine-grained token: add the permission to the token. Not Found (404) on a private resource often means the token can’t see it at all. For image builds that fail, see failed to compute cache key. On GitLab, a job waiting forever is This job is stuck.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.