GitHub App

A GitHub App is an installable integration with its own identity and scoped permissions that acts on a repository without using a human account.

A GitHub App is an installable integration with its own identity and its own scoped permissions, which acts on a repository without using a human account. An account owner installs the app, chooses which repositories it can reach, and approves the permission set the app declared, after which the app authenticates as itself and its activity is attributed to the app rather than to a person.

That separation is why most automation on GitHub is built as an app. An app survives the offboarding of the engineer who set it up, its permissions are written down and reviewable, and the credentials it uses at runtime last an hour.

Definition

A GitHub App has three moving parts: a registration, one or more installations, and a short chain of credentials.

Registration happens once, under a user account or an organization. The registration fixes the app's name, its webhook URL and webhook secret, the repository, organization, and account permissions it asks for with an access level of none, read, or write on each, the webhook events it subscribes to, and whether other accounts may install it. Registration produces an App ID, a client ID, and a private key that the app owner downloads and stores.

Installation is a separate act performed by whoever owns the target account. The installer picks all repositories or a named list, reviews the declared permissions, and accepts. GitHub creates an installation with its own installation ID. If the app later asks for a permission it did not declare before, every existing installation has to approve the change before the new permission takes effect, so the permission set cannot widen quietly.

Credentials come in a chain, and each link is narrower than the one above it. Details below are from GitHub's authentication with a GitHub App documentation, checked on 2026-08-13.

  1. The app signs a JSON Web Token with its private key. That JWT identifies the app itself, has to expire within 10 minutes of issue, and is accepted only on app level endpoints such as listing installations and minting tokens.
  2. The app exchanges the JWT for an installation access token at POST /app/installations/{installation_id}/access_tokens. The installation token expires after one hour and carries the installation's permissions across the installation's repositories. The request can narrow it further, down to a subset of the repositories or a subset of the permissions.
  3. A user access token, obtained through the web flow, acts on behalf of a signed-in person. Every request made with it is limited by the app's permissions and by what that person can already reach, whichever is tighter.

Because the app is its own actor, its writes are attributed to a bot account named after the app, in the form app-slug[bot]. Commits, comments, checks, and audit log entries all carry that name. The app consumes no seat and no license.

One familiar credential is already an example of this shape. The GITHUB_TOKEN available inside a workflow run is an installation token minted automatically for the run, scoped to the repository that owns the workflow, and revoked when the job finishes.

Example

A platform that supplies self-hosted runners has to learn that a job is queued and then attach a machine to the right pool. As a GitHub App, that is a small and legible permission set. The permission names below are GitHub's own, from the permissions required for GitHub Apps reference and the webhook events reference, checked on 2026-08-13.

Permission or eventAccessWhat it buys
Repository: MetadataReadMandatory on every app. Basic repository information.
Repository: ActionsReadRead workflow runs and jobs, and receive the workflow_job webhook.
Repository: AdministrationRead and writeManage runners registered at the repository scope.
Organization: Self-hosted runnersRead and writeManage runners and runner groups at the organization scope.
Webhook event: workflow_jobSubscribedDelivery on queued, in_progress, and completed for each job.

The workflow_job payload carries the job's labels array, so the platform reads which pool a job is asking for before any machine has been started. The set leaves out repository Contents, so an app configured this way has no read access to source code.

Inside a workflow, an app is normally used to get a token that reaches further than GITHUB_TOKEN does. GitHub publishes an action for the exchange:

name: sync-manifest
on:
  push:
    branches: [main]

jobs:
  update-downstream:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/create-github-app-token@v2
        id: app-token
        with:
          app-id: ${{ vars.RELEASE_APP_ID }}
          private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
          owner: ${{ github.repository_owner }}
          repositories: platform-manifests

      - uses: actions/checkout@v4
        with:
          repository: ${{ github.repository_owner }}/platform-manifests
          token: ${{ steps.app-token.outputs.token }}

      - run: ./scripts/bump-image-tag.sh

      - run: |
          git commit -am "bump image tag"
          git push

The first step turns an App ID and a private key into an installation token limited to one repository. The checkout and the push use that token, so the commit lands in platform-manifests attributed to the app's bot account, and the credential is dead an hour later. Doing the same job with a personal access token would put an engineer's identity on every automated commit and leave a long-lived credential in the secret store.

Installation tokens against personal access tokens

PropertyInstallation access tokenPersonal access token
Acts asthe app, shown as app-slug[bot]the human who created it
Lifetimeone hour, minted on demandchosen by the creator, up to no expiry for classic tokens
Repository reachthe repositories chosen at install, narrowable per tokenwhat the owner can reach, or a selected list for fine-grained tokens
Permission setdeclared by the app, approved by the installerchosen by the owner and changeable at any time
When the owner leaves the companyunaffectedrevoked with the account
Rate limit budgetper installation, scaling with the size of the organization5,000 requests per hour shared across everything that person runs
Audit trailattributed to the appattributed to the person

Rate limit figures are from GitHub's rate limits for the REST API, checked on 2026-08-13.

An app registered against a GitHub Enterprise instance follows the same shape, with the registration living on the enterprise and shared across the organizations inside it. The GitHub Enterprise App setup documentation walks through one such flow, including the enterprise slug, the host, and the credentials copied back from the App settings page.

FAQ

How is a GitHub App different from a personal access token?

A GitHub App is an actor in its own right. It has its own identity, its own private key, and a permission set that an installer approves at install time, so its actions appear in the audit log under the app rather than under a person. A personal access token carries the identity and the reach of the human who created it, and it stops working when that account is suspended or removed.

How long does a GitHub App installation token last?

One hour. The app signs a JSON Web Token with its private key, exchanges that for an installation access token, and the installation token expires 60 minutes later. The JWT itself has to expire within 10 minutes of issue, so both credentials in the chain are short lived by design (GitHub authentication documentation, checked on 2026-08-13).

What permissions does a GitHub App need to manage self-hosted runners?

Read on repository Metadata, read on repository Actions so it can receive the workflow_job webhook and read run state, read and write on repository Administration to manage repository level runners, and read and write on the organization Self-hosted runners permission for organization level runners and runner groups. Repository Contents is not part of that set.

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.