GITHUB_TOKEN permissions: how to set least privilege
Every job gets a GITHUB_TOKEN. Its scopes come from the permissions block, and if you never write one, from the repository's default setting, which on older repositories and many organisations is read and write for everything.
How the grant is decided
A top-level permissions block applies to every job. A job's own block replaces it for that job: scopes you do not list in the job block are set to none. Writing permissions: {} at the top removes every scope, so each job has to ask for what it needs.
A least-privilege pattern
Start from nothing and grant per job. Only the job that publishes gets write access, and only to the scope it writes.
permissions: {}
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@<sha> # v4
release:
runs-on: ubuntu-latest
permissions:
contents: write # this job pushes the release tag
steps:
- uses: actions/checkout@<sha> # v4Common over-grants
permissions: write-all at the top or on a job. No permissions block at all, so the repository default applies. A write scope on a job whose steps only read. id-token: write on a job with no step that requests an OIDC token. Each one widens what injected code could do with the token.
What Anchorflow checks
Anchorflow builds a grant table for every job, showing which scopes are set by the top-level block and which by the job's own block, and flags write-all, unset permissions, write scopes that no step needs and unused id-token: write. Each finding names the job and the line and says what to change.
Check your workflow