How Do I Set Permissions for GITHUB_TOKEN?

Declare a permissions block at workflow level with the minimum scopes every job needs, then widen it inside the one job that needs write access.

Answer

Declare a permissions block at workflow level holding the minimum scopes every job needs, usually contents: read, then widen it with a job level block inside the single job that needs more. Both levels use the same map of scope name to read, write, or none, documented in GitHub's workflow syntax reference, checked on 2026-08-13.

One rule drives the whole design. Naming any scope in a block sets every scope left out of that block to none, and a job level block replaces the workflow level set outright rather than merging with it. A narrow default at the top plus one widened job therefore gives you a workflow where the publish step holds write access and the twelve test jobs around it hold none. The scope model itself is covered in workflow permissions.

name: build

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v5
      - run: npm ci
      - run: npm test

  publish:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: warp-ubuntu-latest-x64-4x
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v5
      - name: Log in to the container registry
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
      - name: Push the image
        run: |
          docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

The publish job restates contents: read because its own block replaced the workflow level set, so dropping that line would break actions/checkout inside that job while leaving the rest of the run untouched.

Detail

The scopes GitHub Actions jobs usually need

Most workflows touch four or five of the scopes below. Every level in this table comes from the workflow syntax permissions reference and the automatic token authentication guide, both checked on 2026-08-13.

Operation in the jobScope and levelWhere it appears
Clone the repository with actions/checkoutcontents: readEvery job that checks out code
Push a commit or tag, create a releasecontents: writeRelease and version bump jobs
Push an image to ghcr.io or publish a packagepackages: write plus contents: readThe publish job only
Post a commit status through the REST APIstatuses: writeDeployment gates and external reporters
Create or update a check runchecks: writeTest result annotators
Comment on a pull requestpull-requests: writeCoverage and size report bots
Request an OIDC token for cloud authenticationid-token: writeDeploy jobs assuming a cloud role
Upload SARIF results to code scanningsecurity-events: writeScanner jobs

Package publishing needs packages: write on top of the checkout grant, per GitHub's publishing with GitHub Actions documentation, checked on 2026-08-13. Status and check writes are separate scopes, so a job that posts a commit status and also annotates a check run needs both lines.

The failure signature of a missing scope

A missing scope produces a 403 at the step that needed it rather than an error at the top of the run, which is why these failures often land on someone who edited nothing. The messages below map back to a single scope each.

What the failing step printsScope to add
HTTP 403 with Resource not accessible by integration from a REST callThe scope that endpoint requires, such as pull-requests: write for a comment
remote: Permission to owner/repo.git denied to github-actions[bot] followed by a 403 on pushcontents: write
A denied error from ghcr.io on docker pushpackages: write
Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variableid-token: write

The fastest confirmation is the run log itself. Expand the Set up job step of the failing run and read the GITHUB_TOKEN Permissions list it prints, which shows the scopes that run actually received after the block, the repository default, and any organization pin resolved together. If a scope you expected reads none, the cause is almost always another block naming a different scope. The OIDC hardening guide covers the last row, where the token request fails before any cloud call is made.

What sits above the workflow file

A workflow with no permissions key inherits the repository default, or the organization value when an administrator has pinned one. GitHub documents two settings for that default: permissive, which grants write on most scopes, and restricted, which grants read on contents and packages only. Writing an explicit block at workflow level removes the dependence on that setting, so the same file behaves identically in a fork, a new repository, and an organization that later tightens its default.

Two limits hold whatever the file asks for. A run triggered by a pull request from a fork receives a read only token and no repository secrets, per GitHub's secrets documentation, checked on 2026-08-13. And a block can only narrow what the repository default already allows, so a permissions key never grants access an administrator has turned off. Secrets that outlive a single job belong in the patterns described in managing secrets in GitHub Actions.

Where the runner fits

The token is minted by GitHub Actions for the run and reaches the machine as secrets.GITHUB_TOKEN with the scopes already resolved, so the runner platform sits outside the decision. Moving the jobs above onto WarpBuild changes the runs-on value and nothing else in the file. The token behaves the same way on all four.

WarpBuild does not access or store any build secrets. Secrets are stored in your source code repository and are only accessible to your runner environment, which runs in its own virtual machine created on demand and destroyed after the build, per the runner security reference.

Public repositories need one setting change before managed runners accept their jobs: check Allow public repositories on the Default runner group in your GitHub organization settings, described in the public repositories documentation. Fork pull requests on those repositories keep the read only token described above, so a narrow permissions block and a per job virtual machine cover the same risk from two directions.

Splitting a monolithic job into narrow parallel jobs is also one of the levers in speeding up GitHub Actions.

What is the smallest permissions block a build and test workflow needs?

permissions: contents: read at workflow level. That covers actions/checkout, which is the only GitHub API access a build and test job usually makes. Jobs that call the REST API, publish a package, or request an OIDC token add their own block with the scopes from the table above. A job whose steps stay off the GitHub API entirely can take permissions: {}, which grants none on every scope, per the workflow syntax reference.

Why does a step fail with Resource not accessible by integration?

The token is missing the scope that API call requires. Naming any scope sets every scope left out of that block to none, so adding one grant often removes another a later step depended on. Compare the API calls in the failing step against the scopes in the block that applies to that job, and read the GITHUB_TOKEN Permissions list in the Set up job step to see what the run received. The replacement rule is worked through in workflow permissions.

Does moving a job to a managed runner change what the token can do?

No. GitHub Actions mints GITHUB_TOKEN for the run and attaches the resolved scopes before any runner picks the job up, so the credential described in GITHUB_TOKEN arrives with the same grants it had on GitHub-hosted runners. The only workflow edit a runner move needs is the runs-on value.

Should long lived tokens replace GITHUB_TOKEN when a scope is missing?

Usually no. A missing scope is a one line fix in the block, while a personal access token stored as a secret carries broader access, an expiry to track, and a rotation owner. Reach for OIDC with id-token: write when a job needs cloud credentials, and see managing secrets in GitHub Actions for the cases where a stored credential is genuinely required.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.