Best practice · Security

GitHub Actions permissions: use least privilege

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.

How StarSling works

AI agents open the PR

StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.

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.12

Read-only defaults with one narrowly elevated job
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 }}

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
permissions: write-all

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

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 for that boundary. The ci-secure skill checks both broad permissions and write tokens on untrusted triggers.

Runs on your machine. GitHub API optional, for the impostor-SHA check and the dormancy note.

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.

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.

Verify on your repo

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.

Prompt for your coding agent
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?

  1. Inspect every workflow for a top-level permissions: block; flag missing blocks, write-all, and broad workflow-level write scopes.

  2. For each job-level write permission, trace the exact step that consumes it and remove unrelated scopes.

  3. Confirm jobs triggered by outsider-controlled activity do not receive write access; split trusted and untrusted work when they do.

Go further

One fix, all of them, or forever.

You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.

  1. Fix this one thing

    Copy the prompt above

    Hand P5.5 / P14.3 to your coding agent and fix it in your repo today.

  2. Fix everything, once

    Install the ci-secure skill

    One prompt checks every workflow against ten critical exploit-chain attack vectors, explains each reachable path, and lets you choose focused fixes. Open source, runs locally. It is not a comprehensive security audit.

  3. Keep it fixed, forever

    Install the StarSling GitHub App

    Connect GitHub and the fixes stay applied as your CI evolves, with agents that keep inspecting your workflows and opening optimization PRs you review.

More best practices for GitHub Actions

Where to go next in the CI best-practices catalog.

All CI best practices

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. 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.

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.

Sources

1GitHub Actions · secure use reference (opens in new tab)

2GitHub Actions · workflow permissions (opens in new tab)

Last updated 2026-08-19