Fix "Resource not accessible by integration" in GitHub Actions
Your GitHub Actions job fails with 'Resource not accessible by integration' because GITHUB_TOKEN is read-only by default. Grant the exact scope it needs.
Resource not accessible by integration
Your workflow logs a red Error: Resource not accessible by integration and the job dies the moment it tries to write something back: a label, a comment, a commit, a release. The cause is almost always the same. The GITHUB_TOKEN your job runs with is read-only, so any API call that mutates the repository gets a 403. The fix is to grant that token the specific scope the failing step needs, using the permissions key in your workflow. There are three other situations where that fix alone is not enough, and knowing which one you are in saves you an hour of guessing.
What the error actually means
Every workflow run gets a short-lived GITHUB_TOKEN, generated per job and revoked when the job finishes. It authenticates as a bot identity (github-actions[bot]) against the GitHub API. When a step calls the API to change repository state and the token lacks the matching permission, the API answers 403 Resource not accessible by integration. The word “integration” is GitHub API language for the app behind the token, not a hint that some external integration is misconfigured.
So the message is really saying: this token is not allowed to do that. Two things decide what it is allowed to do: the repository or organization default, and any permissions block in your workflow.
Cause 1: the default token is read-only
Since 2023, new repositories default GITHUB_TOKEN to read-only. Many organizations also flip existing repos to read-only as a hardening step, which is the right call. You can confirm the setting under Settings, Actions, General, Workflow permissions. If it says “Read repository contents and packages permissions”, the default token cannot write anything.
Do not fix this at the repository level by switching the default back to read/write. That grants every workflow in the repo broad access it does not need. Instead, grant the scope in the one workflow that needs it:
permissions:
contents: write # push commits or tags
pull-requests: write # comment on or label PRs
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/tag-release.sh
One rule trips people up constantly. The moment you add a permissions block, GitHub switches that scope from permissive defaults to explicit mode: every permission you do not list becomes none. If your job also needs to read packages or write to the Checks API, you have to list those too. A job that reads and writes typically needs a handful of scopes spelled out, not one.
You can also scope permissions per job, which is stricter and what I reach for by default. A build job gets contents: read, and only the publish job gets contents: write:
jobs:
build:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make build
publish:
needs: build
permissions:
contents: write
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make publish
The full list of scopes and their defaults lives in the GitHub Docs on automatic token authentication. Keep least privilege in mind here; over-scoping the token is one of the quiet ways CI becomes a security liability, a theme covered in GitHub Actions Security: How to Stop Secret Leaks in CI/CD.
Cause 2: the pull request came from a fork
This one catches teams with public repos. When a pull_request event fires from a forked repository, GitHub deliberately hands the workflow a read-only GITHUB_TOKEN and withholds secrets, no matter what your permissions block says. An attacker could otherwise open a PR that runs arbitrary code with write access to your repo. The read-only downgrade is a security boundary, not a bug, and you cannot override it with the permissions key.
If you need to write back on a fork PR (post a comment, apply a label), use the pull_request_target event instead. It runs in the context of the base repository, so the token can be granted write scopes:
on:
pull_request_target:
types: [opened, synchronize]
permissions:
pull-requests: write
Handle pull_request_target carefully. It runs with repository secrets available, so never check out and execute untrusted PR code inside it. Check out the base branch, or only run trusted logic like labeling. The safer pattern for anything that needs the PR’s build output is a two-workflow split: an untrusted pull_request job that builds and uploads an artifact, and a trusted workflow_run job that downloads it and writes results back.
Cause 3: the PR was opened by Dependabot
Dependabot PRs look like internal PRs, but since March 2021 GitHub treats workflow runs triggered by Dependabot as if they came from a fork. The GITHUB_TOKEN is read-only and repository secrets are unavailable. Since October 2021 those runs do respect the permissions key, so a workflow that labels or auto-merges Dependabot PRs can work, but you still have to know two things.
First, secrets your job expects are missing on a Dependabot event. Reference secrets.DEPENDABOT_* values from the separate Dependabot secrets store, not the Actions secrets store. Second, the token is still fork-grade, so write operations that GitHub blocks for forks stay blocked. GitHub documents the exact event matrix in Troubleshooting Dependabot on GitHub Actions.
A common working shape for auto-approving patch bumps:
permissions:
contents: write
pull-requests: write
jobs:
automerge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Cause 4: the action creates or approves a PR
Some steps hit a separate switch. Actions that open a pull request or approve one (for example, peter-evans/create-pull-request) need both pull-requests: write on the token and a repository setting that is off by default. Under Settings, Actions, General, look for “Allow GitHub Actions to create and approve pull requests” and enable it. Without that box checked, the API returns the same Resource not accessible by integration even when your permissions block looks correct. This is the one case where the scope is right and the error still fires, so check it early if the token clearly has pull-requests: write.
A diagnosis checklist
Work through this in order. The first match is almost always your fix:
- Read the failing step. Which API call 403’d (contents, issues, pull-requests, packages, deployments)? That names the scope you are missing.
- Is the trigger a fork
pull_requestor a Dependabot PR? If yes, the token is read-only by design. Move the write logic topull_request_targetor aworkflow_runjob (fork) or accept fork-grade limits (Dependabot). - Does the repo default to read-only? Add a
permissionsblock granting only the scope from step 1. - Is the step creating or approving a PR? Enable the repository setting for it.
- Re-run and confirm.
To see what the token actually carries at runtime, print the scopes near the top of the failing job:
$ echo "$GITHUB_TOKEN" | gh auth login --with-token
$ gh api rate_limit -i 2>&1 | grep -i 'x-oauth-scopes' || echo "using GITHUB_TOKEN, scopes set by permissions block"
If the workflow is a Terraform or infrastructure pipeline that comments plans back on PRs, the same permission model applies; a full working example lives in How to Automate Terraform Reviews with GitHub Actions. And if you are moving a mutating workflow behind a gate before it touches production, the patterns in Testing in Production: Guide to Progressive Delivery pair well with least-privilege tokens.
Verify the fix
After adding the scope, re-run the job and confirm the failing API call now succeeds. A green run is the real signal, but you can also confirm intent by reading the run’s permissions in the logs: expand the “Set up job” step, and GitHub prints the resolved GITHUB_TOKEN permissions for that job. If the scope you added shows there and the call still 403s, you are in Cause 2, 3, or 4, not a missing scope. That distinction is the whole game with this error: decide whether the token could carry the permission at all, then whether you actually granted it.
Related articles
Get the next article in your inbox
Practical DevOps tips, tutorials, and guides. No spam, unsubscribe anytime.