Fix 'Resource not accessible by integration' (403) in GitHub Actions
Fix the 'Resource not accessible by integration' 403 in GitHub Actions: set job-level GITHUB_TOKEN permissions, handle fork PRs, and know when to use an App…
At a glance
- Last reviewed
- Versions referenced
actions/checkout@v7- Reading time
- 4 min read
Code samples are not run in a live repository. See our editorial standards.
On this page
Add the missing scope to the permissions block of the failing job. The error means the GITHUB_TOKEN (or a GitHub App token) is valid but lacks the permission the API call needs, so GitHub answers with HTTP 403 and Resource not accessible by integration.
jobs:
comment:
runs-on: ubuntu-latest
permissions:
pull-requests: write
Read the failing step’s log to see which endpoint returned the 403. That tells you which scope to add.
Find the scope you are missing
The error appears when a step calls the GitHub API (through gh, Octokit, actions/github-script or curl) and the token cannot perform that action. Match the endpoint to a permission:
| API action | Permission needed |
|---|---|
| Push commits, create releases or tags | contents: write |
| Comment on or label a PR | pull-requests: write |
| Comment on, label or close an issue | issues: write |
| Create or update check runs | checks: write |
| Publish to GitHub Packages | packages: write |
| Upload SARIF to code scanning | security-events: write |
| Cancel or rerun workflow runs | actions: write |
Permission keys use hyphens: pull-requests, not pull_requests. An unknown key is rejected when the workflow is parsed.
Set permissions on the job
Declare permissions on the job that needs them. Once you set any permissions key, every scope you do not list becomes none, so include contents: read if the job also checks out code.
name: Label PR
on:
pull_request:
jobs:
label:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v7
- name: Add label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: gh pr edit "$PR_NUMBER" --add-label "needs-review"
Without the permissions block, this job fails with Resource not accessible by integration on any repository whose default token is read-only. gh reads the token from GH_TOKEN, so the env line is required.
Check the repository default
Go to Settings > Actions > General > Workflow permissions. The default is either “Read and write permissions” or “Read repository contents and packages permissions”. New repositories and organizations created since February 2023 default to the restricted read option. Older ones may still be on read and write.
Do not rely on the default. A repository or organization admin can change it at any time, and an organization setting overrides the repository one. Declaring permissions in the workflow makes the behavior independent of that setting.
Deny everything by default
To start from zero, set an empty map at workflow level and grant scopes per job. The value none is not valid at this level; use {}.
name: Permissions demo
permissions: {}
on:
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "This job has no token permissions"
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- run: echo "Read-only access to repository contents"
lint inherits the empty map and gets no scopes. test gets only contents: read. A private repository cannot even check out code with permissions: {}, so any job that uses actions/checkout needs contents: read.
Fork and Dependabot pull requests
The pull_request trigger gives workflows from forks a read-only token and no secrets, whatever you write in permissions. Dependabot-triggered runs are also restricted to a read-only token. Adding pull-requests: write will not fix a comment step on a fork PR. This is the most common reason the error persists after you add the right scope.
You have two options:
- Split the work. Run the untrusted build on
pull_request, upload results as an artifact, then post the comment from a second workflow triggered byworkflow_run, which runs in the base repository. - Use
pull_request_target. It runs the workflow from the base branch with a token that honors yourpermissionsblock and has access to secrets.
| Trigger | Code that runs | Token and secrets for fork PRs |
|---|---|---|
pull_request |
Workflow from the PR merge commit | Read-only token, no secrets |
pull_request_target |
Workflow from the base branch | Token per permissions, secrets available |
pull_request_target is dangerous if you check out and run the PR’s code. A step like actions/checkout with ref: ${{ github.event.pull_request.head.sha }} followed by npm install or a build script executes untrusted code with write access and secrets. Use it only for steps that do not run PR code, such as labeling or commenting.
When the GITHUB_TOKEN is not enough
The GITHUB_TOKEN is scoped to the repository running the workflow. It cannot write to other repositories, and it cannot trigger new workflow runs from events it creates (pushes made with it do not start other workflows). It also cannot call APIs outside its permission list. In these cases, use a different credential.
| Credential | Use it when | Pitfall |
|---|---|---|
| GitHub App installation token | You need cross-repository access, organization-level scopes, or events that trigger other workflows | App must be installed on every target repository and granted the permission |
| Fine-grained PAT | A quick, single-owner automation | Tied to a user; breaks when the user leaves or the token expires |
For an App, mint a short-lived installation token inside the job with the actions/create-github-app-token action, passing the App ID and private key from secrets. If you still see Resource not accessible by integration with an App token, open the App’s settings and add the missing repository permission. After you change App permissions, an organization owner must accept the new permissions on the installation, or the token keeps the old set.
Fine-grained PATs are subject to the same model: the token must list the target repository and have the specific permission, such as “Pull requests: Read and write”. A PAT does not bypass permission checks.
Troubleshooting checklist
- Identify the failing API call in the step log and map it to a permission.
- Confirm the job’s
permissionsblock lists that scope withwrite. - Check whether the run came from a fork or Dependabot; if so, the token is read-only regardless of your YAML.
- Check the organization and repository workflow permission settings, since an organization policy can cap what the workflow requests.
- For reusable workflows, make sure the caller grants the permissions; a called workflow can only keep or reduce the caller’s permissions, never raise them.
- If the call targets another repository, switch to an App token or PAT.
Spotted an error? Report it on our Contact page and see our editorial standards for how we correct articles.