# GitHub Actions permissions: Every Scope and What Needs It

> Every GITHUB_TOKEN permission scope, which common operations require each one, and why declaring a permissions block can break steps that previously worked.

Source: https://latchkey.dev/learn/github-actions/github-actions-permissions-reference  
Updated: 2026-08-20

Declaring a `permissions:` block replaces the defaults entirely rather than adding to them, so an incomplete block breaks steps that worked yesterday.

The `GITHUB_TOKEN` is minted per job with a permission set. It comes from repository or organisation defaults unless the workflow declares its own, and the moment it does, the declaration is the complete set.

That replacement behaviour is the single most common cause of a permissions failure appearing right after someone tightened security: the block lists the scope they were thinking about and silently drops the ones that were previously default.

## Every scope

| Scope | Grants | Needed for |
| --- | --- | --- |
| `actions` | Workflows and runs | Cancelling runs, reading artifacts via API |
| `attestations` | Artifact attestations | Provenance and supply-chain attestation |
| `checks` | Check runs and suites | Publishing test results as checks |
| `contents` | Repository contents | Checkout, pushing commits, creating releases |
| `deployments` | Deployments | Creating deployment records |
| `discussions` | Discussions | Commenting on discussions |
| `id-token` | OIDC token | Cloud auth via OIDC. Must be `write` |
| `issues` | Issues | Creating, labelling, commenting on issues |
| `packages` | GitHub Packages | Pushing to GHCR |
| `pages` | GitHub Pages | Deploying Pages |
| `pull-requests` | Pull requests | Commenting, labelling, requesting review |
| `repository-projects` | Projects | Updating project boards |
| `security-events` | Code scanning | Uploading SARIF results |
| `statuses` | Commit statuses | Setting commit status |

> Each is `read`, `write`, or `none`. Shorthand `permissions: read-all` and `write-all` set every scope, and `permissions: {}` removes all of them.

## The replacement rule

```.github/workflows/ci.yml
# Before: repository defaults, whatever they are
# (no permissions block)

# After: contents is the ONLY permission this job has.
# A step that used to comment on a PR now fails with 403.
permissions:
  contents: read

# Correct: list everything the job needs
permissions:
  contents: read
  pull-requests: write
  checks: write
```

> Declare permissions at the job level rather than the workflow level where you can. A workflow-level grant applies to every job, including ones that only run tests and should not hold write scopes.

## What no permissions block can grant

- **Fork pull requests.** A `pull_request` event from a fork gets a read-only token and no access to secrets, by design and regardless of any `permissions:` declaration.
- **Cross-repository access.** `GITHUB_TOKEN` is scoped to the repository running the workflow. Another repository needs a PAT or a GitHub App token.
- **Anything above the repository or organisation default.** If the default is restricted to read, a workflow cannot grant itself write.
- **Triggering another workflow.** Events created using `GITHUB_TOKEN` do not start new workflow runs, which is a loop-prevention rule rather than a permission.

## Common combinations

```.github/workflows/ci.yml
# push a container image to GHCR
permissions:
  contents: read
  packages: write

# authenticate to a cloud provider with OIDC
permissions:
  contents: read
  id-token: write

# comment on a PR and publish check results
permissions:
  contents: read
  pull-requests: write
  checks: write

# upload code scanning results
permissions:
  contents: read
  security-events: write
```

## Diagnose it: what token do you actually have?

Permission failures in Actions are almost never about your repository settings alone. Three things combine: the default `GITHUB_TOKEN` permission set for the repo or organization, the `permissions:` block in the workflow, and whether the event is a fork pull request, which downgrades the token to read-only regardless of everything else.

```.github/workflows/ci.yml
- name: Show the token scopes actually granted
  run: |
    curl -sI -H "Authorization: Bearer $GITHUB_TOKEN" \
      https://api.github.com/ | grep -i "^x-oauth-scopes\|^x-accepted"
    echo "event: ${{ github.event_name }}"
    echo "fork PR: ${{ github.event.pull_request.head.repo.fork }}"
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

> If `fork PR` prints `true`, stop looking at `permissions:`. A fork pull request gets a read-only token by design, and no workflow-level grant can raise it.

## Grant the narrowest permission that works

Declaring a `permissions:` block switches the job from the repository default to exactly what you list, so an incomplete block is a common cause of a new failure right after someone tightened security. List every scope the job needs, not just the one that failed.

```.github/workflows/ci.yml
permissions:
  contents: read        # checkout
  packages: write       # push to GHCR
  id-token: write       # OIDC to a cloud provider
  pull-requests: write  # comment on or label a PR
  checks: write         # publish check runs
```

> Set `permissions` at the job level rather than the workflow level where you can. A workflow-level grant applies to every job, including ones that only run tests.

## FAQ

### Why did adding a permissions block break my workflow?

Because it replaces the defaults rather than adding to them. Once declared, the block is the complete permission set, so any scope you did not list is revoked for that job.

### Why does my fork pull request get a 403?

Fork pull requests receive a read-only `GITHUB_TOKEN` with no access to secrets, by design, so untrusted code cannot mutate the base repository. No `permissions:` declaration can raise it.

### What permission does OIDC need?

`id-token: write`. Without it the cloud credential action cannot request the OIDC token and fails with a generic credentials error rather than a permissions one.

### Why does my workflow not trigger another workflow?

Events created using `GITHUB_TOKEN` do not start new workflow runs. This prevents recursive loops and is not a permission setting; use a PAT or GitHub App token when a downstream trigger is genuinely required.

---

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
