Start from read-only access to repository contents and add only the scopes a job truly uses. In a workflow file, listing any permission sets every unlisted permission to none, so a short explicit block is both the simplest and the safest setup.
What the default is
Every workflow run gets a temporary GITHUB_TOKEN. GitHub documents two repository defaults. Permissive gives read and write access to all scopes. Restricted gives read access only to the contents and packages scopes.
For repositories on a personal account, GitHub says new repositories default to restricted. Organization repositories inherit the organization setting. You can check yours under Settings, then Actions, then General, in the Workflow permissions section.
How the permissions key behaves
Two rules from the GitHub reference are worth memorizing.
- If you specify access for any permission, every permission you leave out is set to none.
- A key at the top of the file applies to all jobs. A key inside a job applies to that job only.
There are shorthands too: read-all, write-all, and an empty block that disables everything. The docs also warn that an action can read the token through the github.token context even when you do not pass it in, so limiting the token matters.
A pattern for build and publish jobs
Split the work. The build job rarely needs more than reading code. The publish job may need an identity token if you use trusted publishing, which both npm and PyPI document as the way to avoid stored secrets.
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
publish:
needs: build
runs-on: ubuntu-latest
environment: npm
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- run: npm publish
The empty block at the top removes everything by default. Each job then asks for exactly what it needs. The id-token write permission is what npm provenance and trusted publishing require on GitHub Actions, according to the npm docs.
Mistakes to avoid
- Granting write-all because one step failed. Find the one scope that step needs.
- Letting the workflow create and approve pull requests when you do not use that. The setting is off by default for personal repositories.
- Forgetting that anyone with write access can edit the permissions key in a workflow file. Protect the branch that holds your workflows, as described in branch protection for volunteer contributors.
- Trusting third party actions by tag. See pinning GitHub Actions to SHAs correctly.
Fork pull requests
For most pull request events from forks, GitHub changes write permissions to read-only unless an admin enabled the setting that sends write tokens to those workflows. Leave that setting off for a public package unless you have a reason and have read about the risks in pull_request_target risks for maintainers.
Try it yourself
Click through it at your own pace, or open it full screen. Look at what a gate checks before a change is allowed through, then think about which checks your release workflow needs.
Quick check
If I list one permission, what happens to the rest?
They are set to none. GitHub says all permissions you do not specify become none.
Where do I change the repository default?
Settings, then Actions, then General, under Workflow permissions.
Sources
- GitHub docs: Workflow syntax reference
- GitHub docs: Managing GitHub Actions settings for a repository
- GitHub docs: Automatic token authentication
- npm docs: Generating provenance statements
Last reviewed: 19 October 2026.