Set GITHUB_TOKEN permissions per job, starting from the minimum, instead of once for the whole workflow. In a monorepo many services share workflows, so a single broad permissions block gives every job write access it does not need. Default the token to read access and add scopes only where a job requires them.
What the setting does
GitHub's workflow syntax documentation says you can define the token's access by setting read, write or none for each available permission under the permissions key. If you specify access for any one permission, all the ones you do not specify are set to none. You can use read-all or write-all shortcuts, and an empty permissions block disables everything. The documentation also says the permissions key can be placed at the top of a workflow, or inside a specific job, where it changes the default for that job.
The security guidance adds that you should make sure actions only have the minimum access they need, and that it is good practice to set the default permission for the token to read access only at the repository level.
Why monorepos raise the stakes
In separate repos, a release workflow with contents: write belonged to one service. In a shared repo, one workflow file may build ten services, and a top-level permissions: write-all hands the same power to a docs build and a payment deploy alike. A compromised third-party action in any job then runs with that power.
Steps
- Set the repository default for the token to read-only in the repository or organization Actions settings. Org owners can restrict write access at the repository level, as the workflow syntax page notes.
- Add
permissions: {}at the top of each workflow so nothing is granted by default. - Add job-level permissions for exactly what each job does. A test job usually needs
contents: read. A job that comments on pull requests needspull-requests: write. - Give
id-token: writeonly to the job that requests an OIDC token, as described in OIDC trust policies after repo consolidation. - Review any workflow triggered by
pull_request_target. The docs state that the token gets read and write repository permission for that event, even when it comes from a public fork. - Pin third-party actions. The secure-use guidance recommends pinning to a full-length commit SHA, and describes workflow dependency lockfiles as stronger protection for repositories that adopt them.
A small example
permissions: {}
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: npm test
In practice, replace the version tag with a full commit SHA, as the guidance suggests.
Make it stick
Permissions drift as people add jobs. Put workflow files under CODEOWNERS, see CODEOWNERS for workflows and release files, and add a review checklist item for any new permission line. Safeguard's policies, gates and guardrails can check pull requests that change workflow files.
Quick check
If I list one permission, what happens to the rest?
They are set to none. That is why listing only what you need is safe.
Is a workflow-level setting enough?
It works but applies to every job. Job-level permissions let each job hold only what it needs.
Sources
- GitHub Docs: Workflow syntax for GitHub Actions
- GitHub Docs: Secure use reference
- GitHub Docs: Use GITHUB_TOKEN for authentication in workflows
Last reviewed: 19 October 2026.