Skip to content
Latchkey LogoLatchkey home

How GITHUB_TOKEN permissions are set, scope by scope

GITHUB_TOKEN permissions are set with a permissions block at workflow or job level: each of 17 scopes takes read, write or none, and as soon as you name one scope, every scope you did not name becomes none. Start every job at contents: read, the core of the restricted default, and grant writes only on the job that needs them.

Every job gets a fresh GITHUB_TOKEN, an installation token for the GitHub App that Actions installs on your repository, limited to that repository and expiring when the job finishes. What it can do is decided by three things: the default set for your enterprise, organization or repository, the permissions blocks in the workflow file, and the event that triggered the run.

All three are covered here: the block and its rules, every scope GitHub documents with what it allows, where the default comes from and why older organizations still hand out write tokens, and what read-only means on a fork pull request. To generate a block for one job, use the GitHub Actions permissions builder.

Table of 17 GITHUB_TOKEN scopes with values and restricted defaults, beside the four-step calculation
Every scope GitHub documents on 2026-11-12, and the order in which a job's token is computed. Only contents and packages are read in the restricted default.

The permissions block, and the rule that catches people

permissions can sit at the top of the workflow, where it applies to every job, or under jobs.<job_id>, where it applies to that job. The rule in the workflow syntax reference is short and strict: if you specify the access for any permission, all of those that are not specified are set to none. A job-level block therefore replaces the workflow-level block for that job instead of adding to it, so a job that adds pull-requests: write without repeating contents: read can no longer check out a private repository.

write includes read. There are three shorthands: read-all, write-all, and permissions: {}, which sets every scope to none. The pattern that holds up is a read-only default at the top and a short job-level block on the one job that writes.

.github/workflows/ci.yml
permissions:
  contents: read            # the default for every job in this file

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - run: npm ci && npm test

  comment:
    needs: test
    runs-on: ubuntu-latest
    permissions:              # replaces the block above for this job
      contents: read          # repeat it, or checkout of a private repo fails
      pull-requests: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - run: gh pr comment "$PR" --body "Tests passed"
        env:
          GH_TOKEN: ${{ github.token }}
          PR: ${{ github.event.pull_request.number }}

Every scope and what it allows

GitHub documents 17 scopes on 2026-11-12. Most take read, write or none; id-token and copilot-requests take only write or none, and vulnerability-alerts only read or none.

ScopeValuesWhat GitHub says it allows
actionsread, write, noneWork with GitHub Actions; write can cancel a workflow run
artifact-metadataread, write, nonewrite can create storage records for a build artifact
attestationsread, write, nonewrite can generate an artifact attestation for a build
checksread, write, nonewrite can create a check run
code-qualityread, write, nonewrite can upload code coverage reports
contentsread, write, noneread can list commits; write can create a release
copilot-requestswrite, noneLets a workflow in an organization-owned repository use Copilot CLI with GITHUB_TOKEN, where billing policy allows
deploymentsread, write, nonewrite can create a deployment
discussionsread, write, nonewrite can close or delete a discussion
id-tokenwrite, noneFetch an OpenID Connect token
issuesread, write, nonewrite can add a comment to an issue
packagesread, write, nonewrite can upload and publish packages to GitHub Packages
pagesread, write, nonewrite can request a GitHub Pages build
pull-requestsread, write, nonewrite can add a label to a pull request
security-eventsread, write, noneCode scanning alerts: read lists them, write updates their status
statusesread, write, noneread can list the commit statuses for a ref
vulnerability-alertsread, noneread can list Dependabot alerts; included as read by read-all and write-all

Where the default comes from

Before any workflow file is read, the token starts from a default chosen under Settings, Actions, General, Workflow permissions. There are two: read and write access for all permissions, which GitHub calls the permissive setting, and read access for the contents and packages permissions only, the restricted setting. If an enterprise or organization picks restricted, its repositories inherit it and the permissive option is disabled for them.

Which one you have depends on when the owner was created. On February 2, 2023 GitHub made the read-only token the default for new enterprises, new organizations and new personal repositories, and said the change would not affect existing ones. An organization created before then still gives every workflow without a permissions block a write token unless somebody changed the setting. The same panel holds Allow GitHub Actions to create and approve pull requests, off by default for new repositories and organizations.

Terminal
gh api repos/OWNER/REPO/actions/permissions/workflow
{
  "default_workflow_permissions": "read",
  "can_approve_pull_request_reviews": true
}

How GitHub computes a job's token

The workflow syntax reference gives the order, and it explains most surprises.

  1. Start from the default for the enterprise, organization or repository. Restricted at any of those levels applies to the repositories below it.
  2. Apply the workflow-level permissions block, if there is one.
  3. Apply the job-level block, if there is one.
  4. If a pull request event other than pull_request_target from a forked repository triggered the run, and Send write tokens to workflows from pull requests is off, turn every write into read.

What read-only on fork pull requests means

Step 4 is why a workflow that comments on pull requests works for branches in your own repository and fails for forks with the same file. The job asked for pull-requests: write, the fork rule downgraded it to read, and the API answered Resource not accessible by integration. Nothing in the permissions block lifts it; the only override is the Send write tokens to workflows from pull requests setting, which exists for forks of private repositories. GITHUB_TOKEN read-only on fork pull requests walks through that failure.

Two triggers sit on either side of the rule. pull_request_target is exempt: GitHub grants it a read and write token even from a public fork, because it runs the base branch's workflow, which is exactly why it must never run the fork's code. Workflows triggered by Dependabot pull requests get a read-only token and only Dependabot secrets by default, and there the permissions key can raise the access.

.github/workflows/ci.yml
  comment:
    # skip forks instead of failing on them: the token would be read-only anyway
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - run: gh pr comment "$PR" --repo "$REPO" --body "Preview is ready"
        env:
          GH_TOKEN: ${{ github.token }}
          PR: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}

Finding the minimum for a job

Start each job at contents: read, or at permissions: {} for a job that never touches the repository, run it, and add the scope each failure names. The error pages for the common cases say which: a ghcr.io push denied is packages: write, create-pull-request returning 403 is two scopes plus a repository setting, and Resource not accessible by personal access token is the same refusal when the token is not GITHUB_TOKEN at all.

Two things the block cannot do. It cannot reach another repository, since the token is scoped to the one holding the workflow; cross-repository work needs a GitHub App token. And it limits how much a compromised step can do, not which code runs: pinning actions to a commit SHA is the control for that.

Key takeaways

  • Name one scope and every other scope becomes none; a job-level block replaces the workflow-level one.
  • Organizations created before February 2, 2023 may still default to a read and write token.
  • Fork pull requests turn every write into read; pull_request_target is the exception, with a write token.
  • There are 17 scopes on 2026-11-12, and none of them lets GITHUB_TOKEN edit workflow files.

Frequently asked questions

What are the default permissions of GITHUB_TOKEN?
It depends on the setting under Workflow permissions. The restricted default gives read access to contents and packages only; the permissive one gives read and write for all permissions. New organizations and personal repositories have been restricted since February 2, 2023, but older ones kept whatever they had.
Do job-level permissions override workflow-level permissions?
Yes, completely. A job-level block is applied after the workflow-level one, and any scope it does not name is set to none, so repeat contents: read in the job block if the job checks out code.
Why is GITHUB_TOKEN read-only on pull requests from forks?
Because GitHub turns every write into read for pull request events from forks, other than pull_request_target, unless an admin enabled sending write tokens to workflows from pull requests. It stops a stranger's pull request from using your token to push or comment.
How do I give GITHUB_TOKEN permission to push to the repository?
Add contents: write to the job's permissions block. It covers commits, branches and tags, but not changes to files under .github/workflows, which need a GitHub App or personal access token with workflow permission.
What does permissions: write-all do?
It grants write to every scope that accepts it, and read for vulnerability-alerts, which has no write. It is the opposite of least privilege and rarely needed; name the scopes the job uses instead.

References

Least privilege for the token and for the agent. Latchkey keys start read-only and expire. Start free → 30-day trial · No credit card