# Build the smallest GitHub Actions permissions block for each job

> Generate the minimal GitHub Actions permissions block: tick what a job does and get each scope with a one-line reason, from GitHub's documented list.

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

This GitHub Actions permissions generator turns what a job does into the smallest `permissions:` block that lets it do it: tick the tasks and it prints each scope with a one-line reason, using only the scope names and values GitHub documents. Anything you do not tick stays `none`, because naming one scope sets every unnamed scope to `none`.

Most workflows either have no `permissions` block, and inherit whatever default the organization was created with, or have one copied from another file. Neither tells you what the job needs. The way to find out is to list what the job does, map each task to the scope GitHub documents for it, and grant exactly that.

The builder at the foot of this page does the mapping. The table it applies is in the diagram and in the section below, every row comes from GitHub's documentation or the README of the action involved, and nothing leaves your browser. For what each scope means in full, see [GITHUB_TOKEN permissions](/learn/ci-security/github-token-permissions).

## How to use the result

Build one block per job, not one per workflow. A workflow that tests, comments and publishes has three different needs, and a single block at the top hands the publish job's writes to the test job too. Put `permissions: contents: read` at the top of the file and paste each generated block under its `jobs.<job_id>`, where it replaces the top-level block for that job.

Then run the job. If a step fails with a 403 or `Resource not accessible by integration`, the job does something you did not tick: find the task, add it, and keep the reason comment so the next reviewer knows why the scope is there. If a pull request from a fork triggers the job, writes come back as reads whatever the block says, and the builder warns you when you pick that trigger, as it does for `pull_request_target` in a public repository, which GitHub blocks by default from December 2, 2026. A release job that tags, pushes an image and attests it comes out like this:

```.github/workflows/release.yml
jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      artifact-metadata: write  # generate an artifact attestation
      attestations: write       # generate an artifact attestation
      contents: write           # check out the repository; create a GitHub release
      id-token: write           # generate an artifact attestation
      packages: write           # publish a package or push an image to ghcr.io
```

## What the builder maps, and from where

Each of the 20 tasks maps to scopes from one of four sources: the workflow syntax reference, the permission table on the REST endpoint the task calls, GitHub's code scanning documentation, or the README of the action involved. Where either of two scopes works, as with commenting on a pull request, which the REST API accepts with `issues: write` or `pull-requests: write`, the builder picks the one named after the thing you touch.

| Task | Scopes | Source |
| --- | --- | --- |
| Check out the repository | `contents: read` | actions/checkout README |
| Push commits, branches or tags | `contents: write` | Workflow syntax; GitHub App permissions for Git access |
| Create a GitHub release | `contents: write` | Workflow syntax |
| Open a pull request | `contents: write`, `pull-requests: write`, plus the repository setting | peter-evans/create-pull-request README |
| Comment on or label a pull request | `pull-requests: write` | REST: create an issue comment, add labels |
| Comment on or label an issue | `issues: write` | REST: create an issue comment, add labels |
| Publish to GitHub Packages or ghcr.io | `packages: write` | Workflow syntax |
| Request an OIDC token for a cloud login | `id-token: write` | Workflow syntax |
| Generate an artifact attestation | `id-token: write`, `attestations: write`, `artifact-metadata: write` | actions/attest README |
| Deploy to GitHub Pages | `pages: write`, `id-token: write` | actions/deploy-pages README |
| Upload code scanning results (SARIF) | `security-events: write`; in private repositories also `actions: read`, `contents: read` | Uploading a SARIF file to GitHub |
| Read runs or download artifacts from another run | `actions: read` | actions/download-artifact README |
| Cancel workflow runs or delete caches | `actions: write` | Workflow syntax; REST: delete caches |
| Create deployments | `deployments: write` | Workflow syntax |
| Set commit statuses | `statuses: write` | Workflow syntax |
| Create check runs | `checks: write` | Workflow syntax |
| Post to or manage Discussions | `discussions: write` | Workflow syntax |
| Upload coverage reports to code quality | `code-quality: write` | Workflow syntax |
| Read Dependabot alerts | `vulnerability-alerts: read` | Workflow syntax |
| Use Copilot CLI with GITHUB_TOKEN | `copilot-requests: write` | Workflow syntax |

> Every source read 2026-11-12. The builder lists scopes in the order docs.github.com uses, which is alphabetical.

## What a permissions block cannot fix

Some failures look like a missing scope and are a setting. Opening a pull request needs `contents: write` and `pull-requests: write` and also Allow GitHub Actions to create and approve pull requests, in the repository or organization settings. Changing files under `.github/workflows` needs a Workflows permission that no `GITHUB_TOKEN` scope grants, so it takes a GitHub App or personal access token. Another repository is out of the token's reach whatever you grant.

Two things are not governed by `permissions` at all. Uploading and downloading artifacts inside the same run needs no scope, and cache access is set by `cache-mode`. On a job that holds release credentials, set `cache-mode: none` beside its block, for the reasons in [GitHub Actions cache poisoning](/learn/ci-security/github-actions-cache-poisoning).

```.github/workflows/ci.yml
jobs:
  lint:
    runs-on: ubuntu-latest
    permissions: {}          # no scope: this job makes no API calls and clones nothing
    cache-mode: none         # and restores nothing
    steps:
      - run: echo "config checks that need no repository access"
```

## Auditing the workflows you already have

Start with the files that have no block at all, because every job in them inherits the organization default, and for an organization created before February 2, 2023 that can still be read and write on every scope. The first command below lists them. The second lists files with no top-level block, which includes files where every job carries its own; open those and check each job rather than assuming.

For each job in those files, write down what it does in the words the builder uses: checks out code, pushes a tag, comments on a pull request, logs in to a cloud. Tick those tasks, paste the block, and run the workflow on a branch before merging. Then do the same for files whose top-level block is `write-all` or a long list of writes: in our experience those were copied from an example and nobody knows which job needed which write. The diff that results is small and easy to review, and it is the most direct reduction in what a compromised step could do with the token.

```Terminal
# No permissions block anywhere: every job inherits the default
find .github/workflows -name '*.y*ml' -exec grep -L 'permissions:' {} +

# No top-level block: check that each job has its own
find .github/workflows -name '*.y*ml' -exec grep -L '^permissions:' {} +

# Files that grant everything
grep -rl 'permissions: write-all' .github/workflows
```

## FAQ

### What permissions does actions/checkout need?

`contents: read`, according to the action's own README, unless you pass your own `token` or `ssh-key`. If the job has a `permissions` block for anything else, list `contents: read` in it too, because naming any other scope sets `contents` to `none`.

### What permissions do I need to push a Docker image to ghcr.io from GitHub Actions?

`packages: write`, plus `contents: read` if the job checks out code. Add `id-token: write`, `attestations: write` and `artifact-metadata: write` only if the job also attests the image with actions/attest.

### What permissions does a workflow need to comment on a pull request?

`pull-requests: write`, or `issues: write`, since GitHub's comments API accepts either. On a pull request from a fork the token is downgraded to read, so the comment fails there unless the workflow skips forks.

### Does actions/cache need a GITHUB_TOKEN permission?

No scope in the `permissions` block covers the cache. Cache access is set by `cache-mode`, which takes `read`, `write`, `write-only` or `none` per workflow or job, and defaults to read-only for low-trust triggers since June 26, 2026.

### Which permissions does a GitHub Pages deployment need?

`pages: write` and `id-token: write`, as the actions/deploy-pages README states. The id-token permission lets the deployment prove which workflow it came from.

## References

- [Workflow syntax: permissions, every scope and value (read 2026-11-12)](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions)
- [REST API: issue comments, fine-grained permission requirements (read 2026-11-12)](https://docs.github.com/en/rest/issues/comments)
- [actions/attest README: required permissions (read 2026-11-12)](https://github.com/actions/attest)
- [actions/deploy-pages README: required permissions (read 2026-11-12)](https://github.com/actions/deploy-pages)
- [Uploading a SARIF file to GitHub (read 2026-11-12)](https://docs.github.com/en/code-security/how-tos/scan-code-for-vulnerabilities/integrate-with-existing-tools/uploading-a-sarif-file-to-github)

---

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
