# How GITHUB_TOKEN permissions are set, scope by scope

> GITHUB_TOKEN permissions in full: all 17 scopes, the restricted and permissive defaults, job-level rules and fork pull requests, with working blocks.

Source: https://latchkey.dev/learn/ci-security/github-token-permissions  
Updated: 2026-11-12

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](/learn/ci-security/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.

```.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`.

| 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 the `GITHUB_TOKEN` list 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.

```Terminal
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.

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](/learn/github-actions/github-actions-token-push-403-fork) 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](/learn/github-actions/github-actions-ghcr-push-denied-packages-write) is `packages: write`, [create-pull-request returning 403](/learn/github-actions/gha-create-pull-request-token-403) is two scopes plus a repository setting, and [Resource not accessible by personal access token](/learn/github-actions/github-actions-resource-not-accessible-fine-grained-pat) 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](/learn/ci-security/pin-github-actions-to-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)](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions)
- [Managing GitHub Actions settings for a repository: GITHUB_TOKEN default permissions (read 2026-11-12)](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository)
- [GitHub changelog: Updating the default GITHUB_TOKEN permissions to read-only (2023-02-02)](https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/)
- [GITHUB_TOKEN concepts, docs.github.com (read 2026-11-12)](https://docs.github.com/en/actions/concepts/security/github_token)
- [REST API: GitHub Actions permissions, default workflow permissions (read 2026-11-12)](https://docs.github.com/en/rest/actions/permissions)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
