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.
- 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.
- 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. - 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 event | Access | What it buys |
|---|---|---|
| Repository: Metadata | Read | Mandatory on every app. Basic repository information. |
| Repository: Actions | Read | Read workflow runs and jobs, and receive the workflow_job webhook. |
| Repository: Administration | Read and write | Manage runners registered at the repository scope. |
| Organization: Self-hosted runners | Read and write | Manage runners and runner groups at the organization scope. |
Webhook event: workflow_job | Subscribed | Delivery 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 pushThe 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
| Property | Installation access token | Personal access token |
|---|---|---|
| Acts as | the app, shown as app-slug[bot] | the human who created it |
| Lifetime | one hour, minted on demand | chosen by the creator, up to no expiry for classic tokens |
| Repository reach | the repositories chosen at install, narrowable per token | what the owner can reach, or a selected list for fine-grained tokens |
| Permission set | declared by the app, approved by the installer | chosen by the owner and changeable at any time |
| When the owner leaves the company | unaffected | revoked with the account |
| Rate limit budget | per installation, scaling with the size of the organization | 5,000 requests per hour shared across everything that person runs |
| Audit trail | attributed to the app | attributed 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.
Related Terms
- Personal access tokens and how their reach differs from an app: the human-owned credential an app installation replaces, and the scopes it carries.
- GITHUB_TOKEN, the installation token minted for each workflow run: the app credential that already exists inside every job, and the permissions block that shapes it.
- Whether managed runners change your GitHub permissions model: which permissions an installed runner platform holds and which it does not.
- GitHub Enterprise App setup documentation: registering an app on a GitHub Enterprise instance and installing it on an organization.
- Runner security documentation: how runner isolation, storage, and secrets handling are documented.
- WarpBuild pricing: per minute rates by runner type.
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.