The GITHUB_TOKEN should start as read-only for repository contents, and each job should then ask for the extra scopes it truly needs, such as pull requests write or packages write. GitHub's own guidance recommends this default, because any step in a job, including third-party actions, can read the token.
What the token is
Every workflow run gets a short-lived token called GITHUB_TOKEN. It lets the run call the GitHub API as the workflow, for things like commenting on a pull request or pushing a package. It expires when the job ends.
The part that surprises people is who can see it. GitHub's documentation states that an action can reach the token through the github.token context even when your workflow never passes it in. So a compromised or careless action in any step has whatever access the token has. That is the whole reason to trim it.
Step 1: set a safe default
In repository settings, under Actions and then General, there is a "Workflow permissions" section. It offers a permissive choice (read and write for everything) and a restricted choice (read for contents and packages only). Pick the restricted one.
GitHub's docs say new repositories in a personal account already start restricted. New repositories in an organization inherit whatever the organization setting says, and a stricter organization setting disables the permissive option for its repositories. So for a small team the quickest win is to set it once at the organization level.
Step 2: declare permissions in the workflow
Add a permissions block at the top of each workflow file. Give the whole file the minimum, then widen a single job.
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
comment:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- run: echo "post the test summary here"
The job level block replaces the workflow level one for that job, so list every scope the job needs, including contents.
Step 3: handle what breaks
Tightening the token will break a few workflows the first time. The log usually tells you which scope was refused. Typical cases:
| Symptom | Scope to add, on that job only |
|---|---|
| Cannot comment on a pull request | pull-requests: write |
| Cannot open or label an issue | issues: write |
| Cannot push a package to the registry | packages: write |
| Cannot push a commit or tag | contents: write |
Resist the urge to flip the repository default back to permissive. Fix the one job.
When the token is not enough
If a job needs access the token cannot have, GitHub's docs point to two options: a GitHub App that mints an installation token inside the workflow, or a personal access token stored as a secret. Prefer the App, because a personal token is tied to a person and usually broader than the job needs. See our post on which CI credential a small team should use for the trade offs.
A related setting is worth checking in the same visit. GitHub lets you block workflows from creating or approving pull requests. Left on, automation can approve its own changes. The docs flag that as a risk if the pull request merges without real oversight.
How Safeguard fits
Safeguard's policies and gates can fail a pull request that adds a vulnerable dependency, and the free scan at gold.safeguard.sh/scan gives a first look at a repository. Token scoping itself is a setting you control in GitHub.
Quick check
Does a read-only token stop a malicious action from stealing it?
No. It limits what a stolen token can do, but the action can still read secrets you pass to that job. Pin actions and keep secrets out of jobs that do not need them.
Do I need permissions in every workflow if the default is restricted?
It is still good practice. An explicit block documents intent and protects you if someone later loosens the repository default.
Sources
- Use GITHUB_TOKEN for authentication in workflows, GitHub Docs
- Secure use reference, GitHub Docs
- Managing GitHub Actions settings for a repository, GitHub Docs
Last reviewed: 19 October 2026.