Safeguard
Security Guides•

Set GITHUB_TOKEN to read-only and grant permissions per job

Why the default GITHUB_TOKEN should be read-only, how to grant write access per job with the permissions key, and what to check in older workflows.

Safeguard Security Team
3 min read

Set the default GITHUB_TOKEN permission for your repositories to read-only on contents, then add the specific permissions each job needs with the permissions key. A compromised step can only do what the token allows, so a narrow token turns a serious incident into a small one.

Why the token matters

Every workflow run gets a GITHUB_TOKEN. Steps in the job, including third party actions, can use it. If an action is tampered with, or an injected command runs, the token is the first credential in reach.

GitHub's hardening guide states that anyone with write access to a repository can read all its secrets, and advises that credentials carry only the privileges they need. It recommends setting the default token permission to read-only for repository contents and granting more per job.

How to do it

There are two layers.

First, the repository or organization setting that controls the default. Set it to the restrictive option so that a workflow without any permissions block starts read-only.

Second, the permissions key in the workflow file. GitHub documents that you can set it for the whole workflow or for individual jobs. Job level is the better habit, because a build job and a release job rarely need the same access.

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make test

  comment:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - run: echo "post a summary"

The top level block keeps the default tight. The comment job opts into one extra permission and nothing else.

Check what you have today

Older workflows may have been written when broader defaults applied, and they may rely on them silently. Switching the default to read-only can break a job that quietly pushes a tag or labels an issue.

A safe rollout looks like this:

  1. List workflows that have no permissions block.
  2. Switch the default to read-only in a test repository first.
  3. Fix failures by adding the one permission the job needs, at job level.
  4. Roll the setting out to the organization.

I could not confirm from the pages I fetched which default new repositories get today, so check your own settings page instead of assuming.

Fork pull requests

GitHub documents that in pull requests from forked repositories the GITHUB_TOKEN has read-only permissions and secrets are not passed to the runner. That is a platform guard, but it does not cover every trigger. See pull_request versus pull_request_target.

Add a review step

Narrow tokens are only useful if the permissions block cannot be widened unnoticed. Put the workflows directory under CODEOWNERS so edits need approval.

Safeguard policies can enforce checks on pull requests that touch pipeline files. You can try the free scan at gold.safeguard.sh/scan.

Quick check

Does a job level permissions block replace the top level one?

It sets the permissions for that job. Declare every permission the job needs there instead of relying on the top level block.

Is read-only enough for secret safety?

No. The token is one credential. Repository and environment secrets still need their own limits.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.