GITHUB_TOKEN
GITHUB_TOKEN is the GitHub App installation token created at the start of every workflow job, scoped to one repository and expiring when the job ends.
GITHUB_TOKEN is the credential GitHub creates at the start of every workflow job so that steps can authenticate to GitHub as the repository the job is running in. It is a GitHub App installation access token whose permissions stop at that repository, and it expires when the job finishes (GitHub documentation on GITHUB_TOKEN, checked on 2026-08-13).
Nothing has to be created or rotated to get one. The questions worth answering about GITHUB_TOKEN are what it is allowed to do inside a job, how long it lasts, and why a workflow that pushed a commit with it did not trigger the workflow that watches for pushes.
Definition
Three properties describe the credential.
- Created per job. GitHub fetches a fresh installation access token before each job begins. Two jobs in the same run hold two different tokens, and a rerun gets another one.
- Scoped to one repository. The token authenticates as the GitHub Actions app installed on the repository that contains the workflow. It reaches no other repository in the organization.
- Short lived. Expiry is tied to the job rather than to a calendar. The token dies when the job ends, and it has a ceiling even when the job keeps running.
Where the token appears in a job
Steps read the token as ${{ secrets.GITHUB_TOKEN }}, the same syntax used for secrets an operator stored. It never appears in the repository secrets list, because GitHub mints it rather than storing it.
An action can also read the token through the github.token context even when the workflow does not pass it in, which is why granting narrow permissions matters more than controlling which steps receive the value (GitHub tutorial on authenticating with GITHUB_TOKEN, checked on 2026-08-13). Shell steps usually export it as GH_TOKEN for the GitHub CLI or send it in an Authorization: Bearer header for REST calls.
What the token is allowed to do
Two settings decide the starting point. An organization or repository owner picks either the permissive default, which grants read and write across all scopes, or the restricted default, which grants read access to the contents and packages scopes alone. A new organization starts restricted (GitHub organization settings for Actions, checked on 2026-08-13).
The permissions key in the workflow file then adjusts that starting point for a whole workflow or for one job. Each scope takes read, write, or none, and write includes read. Naming any scope sets every scope left unnamed to none, so a short block is a tight block. The table below lists the scopes reached for most often, with what write unlocks (GitHub workflow syntax reference, checked on 2026-08-13).
| Scope | What write allows a step to do | Typical step that needs it |
|---|---|---|
contents | Create a release, push a commit or tag | actions/checkout needs read; a release job needs write |
issues | Comment on, label, or close an issue | Triage automation |
pull-requests | Label, comment on, or update a pull request | A bot that posts build output on a pull request |
checks | Create and update check runs | A test reporter publishing results |
statuses | Set a commit status | A job reporting a status back to a commit |
packages | Upload and publish to GitHub Packages | A publish step pushing a package or container image |
deployments | Create a deployment | A job recording a deploy against an environment |
actions | Cancel a workflow run, read run metadata | A workflow that cancels older runs |
security-events | Update a code scanning alert | A scanner uploading SARIF results |
id-token | Request an OpenID Connect token | A step exchanging that token with a cloud provider |
attestations | Generate an artifact attestation | A build signing what it produced |
vulnerability-alerts | Read only, write is invalid | A job listing Dependabot alerts |
Limits worth knowing before the first API call
| Limit | Value | Where it bites |
|---|---|---|
| Expiry on a GitHub-hosted runner | Job end, 6 hours maximum | Long integration suites near the job ceiling |
| Expiry on a self-hosted runner | Job end, refreshable for 24 hours | Jobs allowed to run up to 5 days |
| REST rate limit | 1,000 requests per hour per repository | Matrix jobs that all poll the API |
| REST rate limit, GitHub Enterprise Cloud resources | 15,000 requests per hour per repository | Large organizations on Enterprise Cloud |
| Workflow triggering | Events raised by the token start no new run | A push step that expects the push workflow to fire |
Sources for the table: the GITHUB_TOKEN concept page and the REST rate limit reference, both checked on 2026-08-13.
The last row causes the most confusion. A job that commits with the token and expects a downstream workflow to pick the commit up sits waiting forever, because GitHub suppresses those events to stop a workflow from triggering itself. workflow_dispatch and repository_dispatch are always exempt, and pull_request events of type opened, synchronize, or reopened produce runs that wait for a maintainer to approve them.
Example
This workflow labels every new issue by calling the REST API with the token, and it states the two scopes the job needs.
name: label-new-issues
on:
issues:
types: [opened]
jobs:
triage:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Add a triage label
run: |
curl --request POST \
--url "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/labels" \
--header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
--header "Accept: application/vnd.github+json" \
--data '{"labels":["triage"]}' \
--failRead the permissions block as the whole allowance for this job. issues: write is what makes the POST succeed. contents: read covers reading repository content. Every other scope is set to none because the block names two, so the same token fails a request to publish a package or create a deployment while this job runs.
Writing the block pays off even when the organization default is already restrictive. The workflow file then records what the job needs, a reviewer sees the allowance in the diff, and the job keeps working after someone flips the organization default to permissive.
A second pattern sets the floor at the top of the file and raises it per job:
name: release
on:
push:
tags: ["v*"]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: make build
publish:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@v4
- run: ./scripts/publish.shpermissions: {} at the top sets every scope to none for any job that stays silent. The build job asks for read access to check out the tag. The publish job adds packages: write to push the artifact and id-token: write so a step can request an OpenID Connect token and exchange it with a cloud provider for short lived credentials, which removes the need for a stored cloud key.
Related Terms
- Workflow permissions and how each scope narrows the token: the
permissionskey, its scope values, and how workflow level and job level blocks interact. - How to set permissions for the GITHUB_TOKEN in a workflow: the smallest correct block for common job shapes, with the settings that override it.
- OIDC token exchange and the id-token permission that enables it: what a workflow trades the OpenID Connect token for, and how a cloud provider validates the claims.
- WarpBuild runner security documentation: how runner isolation, storage, and secrets handling are documented.
- WarpBuild quick start: pointing a first workflow at a managed runner fleet.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What is the GITHUB_TOKEN in GitHub Actions?
GitHub creates a GITHUB_TOKEN secret at the start of each workflow job. It is a GitHub App installation access token for the GitHub Actions app installed on the repository, its permissions are limited to the repository that contains the workflow, and steps read it as secrets.GITHUB_TOKEN or through the github.token context.
When does the GITHUB_TOKEN expire?
The token expires when the job finishes, or at its effective maximum lifetime if the job is still running. On GitHub-hosted runners the maximum job execution time is 6 hours, so the token lives 6 hours at most. On self-hosted runners a job may run up to 5 days, but the installation token can be refreshed for only 24 hours, so a longer job needs a different credential.
Why did a push made with the GITHUB_TOKEN not start another workflow run?
Events raised by the GITHUB_TOKEN do not create new workflow runs, which is how GitHub prevents recursion. The exceptions are workflow_dispatch and repository_dispatch, which always create runs, and pull_request events with the opened, synchronize, or reopened activity types, which create runs in an approval-required state.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.