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…

Published

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 by workflow_run, which runs in the base repository.
  • Use pull_request_target. It runs the workflow from the base branch with a token that honors your permissions block 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

  1. Identify the failing API call in the step log and map it to a permission.
  2. Confirm the job’s permissions block lists that scope with write.
  3. Check whether the run came from a fork or Dependabot; if so, the token is read-only regardless of your YAML.
  4. Check the organization and repository workflow permission settings, since an organization policy can cap what the workflow requests.
  5. 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.
  6. 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.