GITHUB_TOKEN permissions: the minimal set for CI
Defines the minimal GITHUB_TOKEN permissions for CI workflows. Use when workflows fail with resource not accessible by integration, you want least-privilege tokens, or auditing workflow permissions. Covers the permissions block, per-job scoping, and common missing scopes. Not for personal access tokens, GitHub App permissions, or OIDC cloud auth.
TL;DR
The default GITHUB_TOKEN is broader than most jobs need, and the fix is a permissions: block: default to contents: read at the top of the workflow and add only what each job uses. Most CI jobs need just contents: read and actions: read; publishing, commenting, and deployments each add one scope. Least privilege here is one block per workflow, not per repository setting hunting.
Error / query
GITHUB_TOKEN permissions: the minimal set for CIUse this skill when
- Workflows fail with
Resource not accessible by integration - You want least-privilege tokens for CI
- Auditing what a workflow can actually do
- A job needs one extra scope and you want the exact name
Not for this skill when
- You need a personal access token (different credential)
- Configuring a GitHub App's permissions (different surface)
- Authenticating to cloud providers (use OIDC, not the token)
Steps
Step 1: Set restrictive defaults at the workflow top level
permissions:
contents: readExpected: every job in the workflow inherits read-only contents access unless overridden. This is the baseline; jobs that need nothing else are done here.
Step 2: Add per-job scopes only where needed
jobs:
build:
permissions:
contents: read
actions: read
release:
permissions:
contents: write
packages: writeExpected: the build job can read code and artifacts; the release job can additionally push tags and publish packages. Job-level blocks replace (not merge with) the top-level block, so restate contents: read in each job.
Step 3: Map common CI tasks to their scopes
checkout code -> contents: read
download/upload artifacts -> actions: read / write
comment on PRs -> pull-requests: write
create releases -> contents: write
push container images -> packages: write
request OIDC tokens -> id-token permission set to write
cache access -> actions: writeExpected: you can translate any Resource not accessible error into the missing scope by matching the action to this table.
Step 4: Verify by running and reading the error
gh run view [run-id] --log-failed | grep -i "resource not accessible" | head -3Expected: if a scope is still missing, the error names the resource. Add exactly that scope to the job, re-run, and confirm green. Iterate until the minimal set passes.
Variant phrasings
"github token resource not accessible by integration"
A missing scope. Step 3 maps the failing action to the permission to add.
"least privilege github actions token"
Steps 1-2: restrictive top-level defaults plus per-job additions.
Why it happens
The automatic token's permissions come from a matrix of repository settings, enterprise policies, and workflow-declared blocks, and the effective set is the intersection. When a job fails on permissions, the cause is almost always a scope that was never declared, not a broken token. Declaring the full set explicitly makes the requirement visible and reviewable.
Edge cases and pitfalls
- Organization and enterprise settings can cap the token below what the workflow declares; if a declared scope still fails, check the org's workflow permission policy.
permissions: {}(empty block) revokes everything includingcontents: read; checkout then fails. Always include at leastcontents: read.- Reusable workflows (
workflow_call) need their own permissions blocks; they do not inherit the caller's. - Third-party actions may need scopes beyond your job's own (e.g. commenting actions need
pull-requests: write); read the action's README for its required scopes.
Provenance
Resolved from the public thread: https://vectle.com/posts/pstHMTq2n4HrLrySudwRmg0Q
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.