How does the GITHUB_TOKEN work in GitHub Actions, what are its permission boundaries, and how do you handle scenarios requiring elevated or cross-repository access?
Quick Answer
GITHUB_TOKEN is an installation token automatically generated per workflow run, scoped to the triggering repository with permissions defined at the workflow or job level. For cross-repo access or elevated privileges, you must use a GitHub App installation token or a personal access token (PAT) stored as a secret.
Detailed Answer
Consider GITHUB_TOKEN as a temporary building pass issued to a contractor for a single day's work at one specific building. The pass grants access only to the rooms listed on it (permissions), it expires when the workday ends (workflow run completes), and it cannot be used to enter other buildings (repositories). If the contractor needs access to another building, the property management company (a GitHub App or PAT) must issue a separate pass.
GITHUB_TOKEN is an installation access token created automatically by GitHub at the start of each workflow run. It is generated from the internal GitHub Actions app installed on the repository and is scoped exclusively to that repository. The token is available as 'secrets.GITHUB_TOKEN' or the preferred 'github.token' context, and it expires when the workflow run completes or after 24 hours, whichever comes first. Each job in a workflow gets its own unique token instance, and the token is revoked the moment the job finishes.
The permissions model operates on two levels. The default permissions are set at the organization or repository level in Settings → Actions → General → Workflow permissions, which can be either 'Read and write' (permissive default) or 'Read repository contents and packages' (restricted default). On top of this, individual workflows and jobs can declare explicit permissions using the 'permissions' key, which overrides the defaults entirely—not additively. If you set 'permissions: { contents: read }' at the workflow level, you get ONLY contents:read and nothing else. This 'all-or-nothing' override behavior catches many engineers off guard.
In production, the payments-api team should adopt the principle of least privilege by setting the organization default to restricted and declaring explicit permissions in every workflow. For common scenarios: deployments need 'deployments: write' and 'contents: read'; publishing packages requires 'packages: write'; creating releases needs 'contents: write'; and commenting on PRs requires 'pull-requests: write'. The 'id-token: write' permission is special—it enables OIDC token generation for keyless authentication with cloud providers like AWS, GCP, and Azure, eliminating the need to store cloud credentials as secrets.
The most critical limitation of GITHUB_TOKEN is its single-repository scope. It cannot read code from, write to, or trigger workflows in other repositories—even within the same organization. This creates a hard boundary for cross-repo operations like triggering a downstream deployment after a library is published or reading shared configuration from a central repository. The solutions, in order of preference, are: (1) a GitHub App installation token created via actions/create-github-app-token with cross-repo permissions, (2) a fine-grained PAT stored as an organization secret, or (3) repository dispatch events triggered by a GitHub App. Never use a classic PAT with broad 'repo' scope—this is an antipattern that gives workflows access to every repository the token owner can access. Another subtle gotcha: GITHUB_TOKEN cannot trigger new workflow runs when used with git push or PR creation, specifically to prevent infinite workflow loops. If you need push-triggered cascading workflows, use a GitHub App token instead.