Build the smallest GitHub Actions permissions block for each job
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.

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:
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.ioWhat 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 |
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.
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.
# 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/workflowsWhat the job does
permissions: contents: read # check out the repository
The default output is the block for a job that only checks out code. With JavaScript switched off, the table under "What the builder maps, and from where" lists the scope for every task: take the highest level each scope needs, write it once, and leave every other scope out. Scope names and values are from docs.github.com workflow syntax, read November 12, 2026.
Key takeaways
- Build one block per job and keep
contents: readat the top of the file. - Every scope in the output carries the task that needs it; keep those comments in the workflow.
- Fork pull requests turn writes into reads, and
pull_request_targetgets writes: pick the trigger before trusting the block. - Settings, workflow-file edits and other repositories are outside what any block can grant.
Frequently asked questions
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?
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.Related guides
References
- Workflow syntax: permissions, every scope and value (read 2026-11-12)
- REST API: issue comments, fine-grained permission requirements (read 2026-11-12)
- actions/attest README: required permissions (read 2026-11-12)
- actions/deploy-pages README: required permissions (read 2026-11-12)
- Uploading a SARIF file to GitHub (read 2026-11-12)
- GitHub Actions documentation