Start with read only for the whole workflow, then grant extra permissions only to the job that publishes. GitHub's documentation advises giving the GITHUB_TOKEN the least required access, and the permissions key can be set for an entire workflow or for individual jobs.
Why release workflows deserve care
A release workflow can create tags, publish files, and often hold the strongest credentials in the project. If an attacker gets code running in it, through a poisoned dependency or an unsafe pull request trigger, the permissions you granted define the damage.
What GitHub's documentation says
The page on controlling GITHUB_TOKEN permissions explains that you use the permissions key in the workflow file to modify permissions, for the whole workflow or for individual jobs, so you can configure the minimum required. It calls granting the least required access a good security practice. If you need a token with permissions the GITHUB_TOKEN cannot have, it suggests creating a GitHub App and generating an installation access token inside the workflow.
A split that works
Separate building from publishing into two jobs with different permissions.
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<full commit SHA>
- run: make dist
- uses: actions/upload-artifact@<full commit SHA>
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@<full commit SHA>
with:
name: dist
- run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Treat this as a pattern, not a drop in file. The placeholders must be replaced with real pinned commits.
Rules of thumb
| Practice | Reason |
|---|---|
| Top level permissions set to read | Any job added later starts with little |
| Write scopes only on the publish job | Limits what a compromised build step can do |
| Build job runs no publish credentials | Untrusted build inputs cannot reach them |
| Publish job triggered by tags or protected branches | Pull requests from forks cannot start it |
| Pin third party actions to commit SHAs | Tags can move, see pin GitHub Actions to a SHA or tag |
Add a review gate
Publishing to a registry is hard to undo. Require a human approval for the publish job using an environment with required reviewers. See GitHub environments required reviewers.
Try it yourself
Click through it at your own pace, or open it full screen.
Watch how a gate blocks a change that does not meet policy. A permissions rule on your release workflow is a gate of the same kind.
Checking what you have today
Open each workflow that publishes and read its permissions block. If there is none, the repository default applies, and you should check what that default is in your settings. Search for workflows that run on pull requests from forks and make sure none of them can reach the publish credentials. Finally, list who can edit workflow files and consider requiring review from the owners of those files. Permissions in the file only help if changing the file is itself controlled.
Quick check
Why not give the whole workflow write access for simplicity?
Because every step, including third party ones, would then run with it. Narrow scopes limit the damage of one bad step.
Does the build job need the registry token?
No. Give it to the publish job only, or use short lived credentials where your registry supports them.
Sources
Last reviewed: 19 October 2026.