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

ScopeWhat write allows a step to doTypical step that needs it
contentsCreate a release, push a commit or tagactions/checkout needs read; a release job needs write
issuesComment on, label, or close an issueTriage automation
pull-requestsLabel, comment on, or update a pull requestA bot that posts build output on a pull request
checksCreate and update check runsA test reporter publishing results
statusesSet a commit statusA job reporting a status back to a commit
packagesUpload and publish to GitHub PackagesA publish step pushing a package or container image
deploymentsCreate a deploymentA job recording a deploy against an environment
actionsCancel a workflow run, read run metadataA workflow that cancels older runs
security-eventsUpdate a code scanning alertA scanner uploading SARIF results
id-tokenRequest an OpenID Connect tokenA step exchanging that token with a cloud provider
attestationsGenerate an artifact attestationA build signing what it produced
vulnerability-alertsRead only, write is invalidA job listing Dependabot alerts

Limits worth knowing before the first API call

LimitValueWhere it bites
Expiry on a GitHub-hosted runnerJob end, 6 hours maximumLong integration suites near the job ceiling
Expiry on a self-hosted runnerJob end, refreshable for 24 hoursJobs allowed to run up to 5 days
REST rate limit1,000 requests per hour per repositoryMatrix jobs that all poll the API
REST rate limit, GitHub Enterprise Cloud resources15,000 requests per hour per repositoryLarge organizations on Enterprise Cloud
Workflow triggeringEvents raised by the token start no new runA 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"]}' \
            --fail

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

permissions: {} 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.

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.