---
title: "GitHub Actions permissions: use least privilege"
description: "Limit GitHub Actions GITHUB_TOKEN permissions with read-only workflow defaults and narrowly scoped job-level write access. Includes copyable YAML."
url: https://starsling.dev/best-practices/github-actions/limit-github-actions-permissions
canonicalUrl: https://starsling.dev/best-practices/github-actions/limit-github-actions-permissions
---

# GitHub Actions permissions: use least privilege

- [How StarSling works](https://starsling.dev/)

Best practice: Security. Last updated: 2026-08-19

Set a read-only top-level `permissions:` default in every GitHub Actions workflow, then grant each job only the additional `GITHUB_TOKEN` scopes it needs, so unrelated steps cannot inherit repository write access.

## Table of contents

- [Do this: start read-only and elevate one job](#do-this)
- [Avoid this: grant write access to the whole workflow](#avoid-this)
- [Permissions do not make an unsafe trigger safe](#permissions-on-untrusted-events)
- [Why it matters](#why-it-matters)
- [When to declare GitHub Actions permissions](#when-to-use)
- [Verify on your repo](#verify)
- [Trying to make GitHub Actions faster?](#speedup-guides)
- [Sources](#sources)

<a id="do-this"></a>

## Do this: start read-only and elevate one job

Every workflow declares a restrictive top-level permissions block. Jobs that only build or test keep `contents: read`; a job that comments, publishes, or updates a status receives only that named write scope. Job-level permissions replace the workflow defaults, so each elevated job also re-declares any read scopes it still needs.

_Read-only defaults with one narrowly elevated job_

```yaml
permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm test

  comment:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - run: gh pr comment "${{ github.event.pull_request.number }}" --body "Tests passed"
        env:
          GH_TOKEN: ${{ github.token }}
```

<a id="avoid-this"></a>

## Avoid this: grant write access to the whole workflow

Every action and install script in every job receives a token with write access it does not need, widening the damage if any executed code is compromised.

_Workflow-wide write access_

```yaml
permissions: write-all

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm install
      - run: npm test
```

<a id="permissions-on-untrusted-events"></a>

## Permissions do not make an unsafe trigger safe

Least privilege limits blast radius, but a write scope still belongs only in a job whose trigger and inputs are trusted. If an outsider can influence the run, separate the privileged operation from the untrusted build. See [secure pull_request_target](/best-practices/github-actions/secure-pull-request-target) for that boundary. The [ci-secure skill](/ci-secure) checks both broad permissions and write tokens on untrusted triggers.

<a id="why-it-matters"></a>

## Why it matters

GitHub creates a repository-scoped token for every job, and actions can access it through the `github.token` context even when it is not passed explicitly. A restrictive permissions block turns a compromised build step from a repository writer into a read-only process unless that specific job genuinely needs more.

<a id="when-to-use"></a>

## When to declare GitHub Actions permissions

**Use it when:** Use an explicit top-level permissions block in every workflow. Add a job-level write scope only for the smallest job that performs the matching operation, such as commenting on a pull request or publishing a package.

**Be careful when:** Do not add a write permission speculatively to silence an error. Identify the API call that needs it, scope it to one job, and keep untrusted code out of that job.

<a id="verify"></a>

## Verify on your repo

Hand this prompt to your coding agent to audit and fix this practice in your own repo:

Audit every workflow under .github/workflows for missing or broad GITHUB_TOKEN permissions. Add a top-level `permissions: { contents: read }` default where appropriate, replace `write-all` and workflow-wide write scopes with the minimum named permissions, and grant write access only in the job that performs the matching operation. Remember that a job-level permissions block replaces workflow defaults, so re-declare required read scopes. Do not put write access in a job an outsider can trigger or influence; split that workflow instead. Preserve behavior, show the diff, and open a PR rather than applying it blindly.

Ground these changes in the upstream docs before you edit: https://docs.github.com/en/actions/reference/security/secure-use, https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.

Prefer to check by hand:

- Inspect every workflow for a top-level `permissions:` block; flag missing blocks, `write-all`, and broad workflow-level write scopes.
- For each job-level write permission, trace the exact step that consumes it and remove unrelated scopes.
- Confirm jobs triggered by outsider-controlled activity do not receive write access; split trusted and untrusted work when they do.

## Go further

- [Install the ci-secure skill](https://starsling.dev/ci-secure) to check every workflow against the ten critical exploit-chain attack vectors.

<a id="more-best-practices"></a>

## More best practices for GitHub Actions

- [Scope id-token: write to the publishing job](https://starsling.dev/best-practices/github-actions/scope-id-token-per-job)
- [Secure pull_request_target in GitHub Actions](https://starsling.dev/best-practices/github-actions/secure-pull-request-target)
- [GitHub Actions secrets: prevent leaks and overexposure](https://starsling.dev/best-practices/github-actions/protect-github-actions-secrets)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

<a id="speedup-guides"></a>

## Trying to make GitHub Actions faster?

If you do not know why a run is slow yet, start with the diagnostic guide. [Let an agent find which of these applies: /ci-speedup](/ci-speedup). If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement.

- [GitHub Actions too slow](https://starsling.dev/github-actions-too-slow)
- [Fast GitHub Actions](https://starsling.dev/fast-github-actions)
- [GitHub Actions alternatives](https://starsling.dev/github-actions-alternatives)
- [GitHub Actions pricing](https://starsling.dev/github-actions-pricing)
- [Self-hosted GitHub Actions runners](https://starsling.dev/self-hosted-github-runners)
- [Docker CI on GitHub Actions](https://starsling.dev/ci/docker)
- [Install the free /ci-score skill to improve your GitHub Actions setup](https://starsling.dev/ci-score)

<a id="faq"></a>

## FAQ

### What permissions should GitHub Actions have by default?

Start with `contents: read`, then add only the named scope a specific job needs. Avoid `write-all`; most build and test jobs need no repository write access.

### Can an action use GITHUB_TOKEN if I do not pass it?

Yes. An action can access the token through the `github.token` context, which is why restricting the job's permissions matters even when your YAML never passes a token input.

<a id="sources"></a>

## Sources

1. [GitHub Actions - secure use reference](https://docs.github.com/en/actions/reference/security/secure-use)
2. [GitHub Actions - workflow permissions](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions)
