Workflow Permissions

Workflow permissions are the per scope grants on the token a GitHub Actions run uses, declared with a permissions key at workflow level or job level.

Workflow permissions in GitHub Actions are the per scope grants attached to the token a workflow run uses, declared with a permissions key at workflow level or at job level. Each scope, such as contents or packages, takes one of three values, read, write, or none, and every step in the run receives a token carrying exactly the grants that key resolved to.

When the key is absent, the run inherits a default set outside the workflow file, at the repository or organization level. That gap between what the file says and what the run receives is where most permission surprises begin.

Definition

A permissions block is a map from scope name to access level. The syntax and the full scope list are documented in GitHub's workflow syntax reference, checked on 2026-08-13.

permissions:
  contents: read
  packages: write

Three shorthand forms exist alongside the map. permissions: read-all grants read on every scope, permissions: write-all grants write on every scope, and permissions: {} grants none on every scope, which fits a job whose steps stay off the GitHub API.

The rule that surprises people

Naming any scope in a block sets every scope left out of that block to none. A block written to add packages: write for a publish step also removes the pull-requests: write a later step relied on, and the failure lands in a step nobody edited. A block replaces the default set outright rather than merging with it.

The same replacement applies between levels. A job level block replaces the workflow level set for that job outright rather than adding to it, so a job that needs one extra scope has to restate the scopes it already had.

What sits above the workflow file

Two limits apply regardless of what the file asks for.

Runs triggered by a pull request from a forked repository receive a read only token. That is the mechanism keeping a contributor's branch from pushing to the base repository or publishing under its name.

An organization can pin the default and restrict who may change Actions settings, so the value a run starts from is an administrative decision made once for many repositories. GitHub documents the two settings in configuring the default GITHUB_TOKEN permissions, checked on 2026-08-13.

The scopes a build usually touches

Write access on a scope includes the read access for that same scope. The table lists what write buys on the scopes that appear most often in build and release workflows.

ScopeWhat write access allows
contentsPush commits, create tags, create releases, update branches
packagesPublish and update packages in the GitHub package registry
pull-requestsOpen, label, comment on, and close pull requests
issuesOpen, label, comment on, and close issues
checksCreate and update check runs
statusesSet commit statuses on a ref
actionsCancel, re-run, and manage workflow runs
deploymentsCreate deployments and deployment statuses
security-eventsUpload code scanning results
id-tokenRequest an OpenID Connect token for cloud authentication

id-token is the one entry with no read level. It accepts write or none, because requesting an identity token is the only operation the scope covers.

The failure signature

A missing scope surfaces as HTTP 403 from the API with the message Resource not accessible by integration. The message names no scope, so diagnosis means matching the API call in the failing step against the block that produced the token. A step calling gh pr comment needs pull-requests: write, a step running npm publish against the GitHub registry needs packages: write, and actions/checkout needs contents: read on a private repository.

Example

This workflow publishes a package on a version tag. Two grants at the top of the file describe the whole run.

name: publish
on:
  push:
    tags: ['v*']

permissions:
  contents: read
  packages: write

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://npm.pkg.github.com
      - run: npm ci
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

contents: read lets actions/checkout fetch the tagged commit. packages: write lets npm publish push the tarball to the registry. Every other scope resolves to none for this run, because the block named two scopes and silence on the rest means denial. Adding a step later that comments on a pull request returns 403 until pull-requests: write joins the block.

What applies when the block is absent

Delete those three lines and the run still starts. What changes is the origin of the grants: the token now carries the repository default under Settings, Actions, General, Workflow permissions, or the organization value when an administrator pinned it. The permissive setting grants write access on most scopes, and the restricted setting grants read access on contents and packages.

So the same file publishes successfully in a permissive repository and fails at npm publish in a restricted one, with no difference between the two workflow files. Writing the block down removes that dependency on a setting the file cannot see.

Read only default with one job widened

The shape most release pipelines settle on is a narrow default at the top and a single job that restates what it needs.

name: release
on:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

  release:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/release.sh
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The test job inherits contents: read and nothing else. The release job replaces that set with three grants of its own, which is why contents: write has to be repeated there even though the top of the file already granted read. id-token: write is present so the release script can exchange an OpenID Connect token for cloud credentials instead of reading a stored key.

FAQ

Where does a permissions block go in a workflow file?

At the top level of the file, inside a single job, or both. A top level block sets the grants for every job in the run. A job level block replaces that set for one job. The common shape is a read only default at the top and one job widened to the scopes it actually calls.

What happens when a workflow has no permissions block?

The run uses the default configured for the repository, or the organization value when an administrator has pinned it. GitHub documents two choices for that default: permissive, which grants write access on most scopes, and restricted, which grants read access on contents and packages. The workflow file carries no signal about which one applies, so the same file behaves differently across repositories.

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

The token is missing the scope that API call requires. Naming any scope in a permissions block drops every scope left out to none, so adding one grant can remove another that a later step depended on. Match the API calls in the failing step against the scopes in the block and add the missing one.

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.