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 j
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 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.
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.
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.
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.
| Scope | Values | What GitHub says it allows |
|---|---|---|
actions |
read, write, none | Work with GitHub Actions; write can cancel a workflow run |
artifact-metadata |
read, write, none |
write can create storage records for a build artifact |
attestations |
read, write, none |
write can generate an artifact attestation for a build |
checks |
read, write, none |
write can create a check run |
code-quality |
read, write, none |
write can upload code coverage reports |
contents |
read, write, none |
read can list commits; write can create a release |
copilot-requests |
write, none | Lets a workflow in an organization-owned repository use Copilot CLI with GITHUB_TOKEN, where billing policy allows |
deployments |
read, write, none |
write can create a deployment |
discussions |
read, write, none |
write can close or delete a discussion |
id-token |
write, none | Fetch an OpenID Connect token |
issues |
read, write, none |
write can add a comment to an issue |
packages |
read, write, none |
write can upload and publish packages to GitHub Packages |
pages |
read, write, none |
write can request a GitHub Pages build |
pull-requests |
read, write, none |
write can add a label to a pull request |
security-events |
read, write, none | Code scanning alerts: read lists them, write updates their status |
statuses |
read, write, none |
read can list the commit statuses for a ref |
vulnerability-alerts |
read, none |
read can list Dependabot alerts; included as read by read-all and write-all
|
From docs.github.com workflow syntax, read 2026-11-12. There is no scope for files under
.github/workflows: GitHub Apps need a separate Workflows permission to change them, and theGITHUB_TOKENlist has no equivalent, so a push that edits a workflow file needs a GitHub App or personal access token.
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.
gh api repos/OWNER/REPO/actions/permissions/workflow
{
"default_workflow_permissions": "read",
"can_approve_pull_request_reviews": true
}
The response shown is the example from GitHub's REST reference for this endpoint, which needs Administration read on the repository. Default history from the GitHub changelog of 2023-02-02; settings from docs.github.com; both read 2026-11-12.
How GitHub computes a job's token
The workflow syntax reference gives the order, and it explains most surprises.
- Start from the default for the enterprise, organization or repository. Restricted at any of those levels applies to the repositories below it.
- Apply the workflow-level
permissionsblock, if there is one. - Apply the job-level block, if there is one.
- If a pull request event other than
pull_request_targetfrom a forked repository triggered the run, and Send write tokens to workflows from pull requests is off, turn everywriteintoread.
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.
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.
FAQ
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
- Workflow syntax: permissions and how they are calculated (read 2026-11-12)
- Managing GitHub Actions settings for a repository: GITHUB_TOKEN default permissions (read 2026-11-12)
- GitHub changelog: Updating the default GITHUB_TOKEN permissions to read-only (2023-02-02)
- GITHUB_TOKEN concepts, docs.github.com (read 2026-11-12)
- REST API: GitHub Actions permissions, default workflow permissions (read 2026-11-12)
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.