You wired an AI agent into GitHub. Maybe it is Claude Code running an overnight refactor loop, a Cursor background agent that opens PRs while you sleep, an MCP server, or a CrewAI worker that triages issues. You started with the path of least resistance: a classic personal access token, the kind that begins with ghp_. You pasted it into .env, the agent worked, you moved on.
Then one evening you read the scope description more carefully. That repo checkbox you ticked grants the agent full read and write on every repository you can see, public and private, across every organization you belong to. The agent that was supposed to open a PR on myproject can now read your employer's monorepo. It can read every private fork you ever pushed. It can git push --force to anything.
So you go looking for the "right" answer and GitHub hands you four parallel options without ranking them: classic PATs, fine-grained PATs, GitHub Apps, and OAuth user tokens. The docs treat them as a menu. They are not a menu. They have very different blast radii, very different lifetimes, and very different audit footprints. This post ranks them for agent use, shows the exact tokens and flows, and is honest about the trade-off most agent posts skip.
The four options at a glance
Before we dig in, here is the matrix. We will spend the rest of the post justifying every cell.
| Option | Token prefix | Default lifetime | Scope granularity | Audit attribution | Revocation | Agent fit |
|---|---|---|---|---|---|---|
| Classic PAT | ghp_ | No expiry by default | Coarse OAuth scopes (repo, workflow, admin:org) | User | Manual, all-or-nothing | Bad |
| Fine-grained PAT | github_pat_ | Up to 366 days for org-scoped | 50+ permissions, per-repo selection | User | Manual, all-or-nothing | OK for one developer, one repo |
| GitHub App installation token | ghs_ | 1 hour, non-configurable | Per-install permissions, narrowable per request | App + install | Revoke install or rotate key | Best for unattended automation |
| GitHub App user-to-server token | ghu_ | 8 hours, refreshable for 6 months | Intersection of app grant and user's permissions | User, tagged as app | Revoke install or user can unauthorize | Best when the agent acts as a real human |
Every claim in that table is sourced below.
Option 1: Classic PATs. Why every "just paste this token" tutorial is wrong for agents
The classic PAT is the original GitHub credential and it shows its age. Scopes are coarse OAuth-style buckets: repo (full control of all repositories the user can see), workflow, admin:org, gist, notifications, and a handful of others. There is no per-repo selector. If you grant repo to your agent, the agent can read every private repo that you can read, in every org you belong to. (Scopes for OAuth apps)
Worse, the default expiry option when you create a classic PAT is "No expiration." That is a credential the agent can hold forever, sitting in .env waiting for a tool call, a log scrape, or a prompt-injection payload to exfiltrate it. (Managing your personal access tokens)
Here is what a classic PAT call looks like, and why you should treat the response as a warning:
curl -H "Authorization: Bearer ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
https://api.github.com/user/repos
# Returns every repo the user can see, public + private, across every org.
If your agent's job is "open a PR on one repo," that response is orders of magnitude more access than the agent needs. GitHub's own documentation recommends against classic PATs and points users at fine-grained PATs or GitHub Apps instead. (Managing your personal access tokens)
There are a handful of features that still require a classic PAT because no fine-grained equivalent has shipped yet: Gists, notifications, some GitHub Packages operations. If you are building an agent that only does those things, you are stuck. For everything else, stop using ghp_ tokens.
For more on why long-lived environment variables are the wrong place for agent credentials in general, see stop putting API keys in environment variables.
GitHub has no announced sunset date for classic PATs as of mid-2026. They still work. "Still works" is not "should be used." Treat any agent stack that requires a classic PAT as a stack that needs rework.
Option 2: Fine-grained PATs. The pragmatic developer compromise
Fine-grained PATs were introduced to fix the blast-radius problem of classic PATs. Instead of coarse OAuth scopes, you get 50+ granular permissions, each settable to no access, read, or read-write. You also get a "Only select repositories" selector, so the token can be scoped to one repo instead of "everything I can see." (Permissions required for fine-grained personal access tokens, Introducing fine-grained personal access tokens)
The minted token starts with github_pat_. Creating one for an agent that opens PRs looks like this:
- Settings, Developer settings, Personal access tokens, Fine-grained tokens, Generate new token
- Resource owner: yourself or your org
- Repository access: "Only select repositories" and pick exactly the one your agent needs
- Permissions:
Contents: Read-only,Pull requests: Read and write. Nothing else. - Expiration: as short as you can tolerate
curl -H "Authorization: Bearer github_pat_xxxxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
https://api.github.com/repos/OWNER/REPO/pulls
That call now returns only what the agent needs, and a leak only burns one repo's worth of access. This is the right answer for the lone developer who wants their agent to do one thing on one repo.
It is not the right answer for unattended automation at scale, and here is why. The default maximum lifetime for an org-scoped fine-grained PAT is 366 days. (Setting a personal access token policy for your organization) That is a credential the agent will hold on disk for up to a year. If your prompt-injection threat model includes the agent being tricked into reading and exfiltrating its own .env (and it should, see how prompt injection becomes credential exfiltration), a 366-day token is a long window for the attacker to keep working.
Org owners can require approval before any fine-grained PAT touches org resources, and you should turn that on if you administer an org. (Setting a personal access token policy for your organization) It does not solve the lifetime problem but it adds a human in the loop at creation time.
Option 3: GitHub Apps. The right answer if you can afford the setup cost
The GitHub App model is structurally different. You register an App once. The App has its own identity, its own private key, and its own permission grant per installation. To call the API, you mint a short-lived JSON Web Token signed with the App's private key, exchange that JWT for an installation access token, and use the installation token for the actual calls.
Installation tokens start with ghs_ and expire after exactly one hour. You cannot configure a longer lifetime. If the agent leaks one, the attacker has at most 60 minutes before the token is dead. (Generating an installation access token for a GitHub App)
The most useful feature for agent use, and one almost no tutorial covers, is per-request permission narrowing. When you mint the installation token, you can pass repositories (or repository_ids) and permissions in the body. The minted token cannot exceed what the App was granted, but it can be made strictly smaller for one task. (Generating an installation access token)
# Step 1: generate a JWT signed with your App's private key (RS256).
# claims: iat (now-60), exp (now+600), iss (App's client ID)
node -e '
const jwt=require("jsonwebtoken"), fs=require("fs");
const pk=fs.readFileSync("app-private-key.pem");
console.log(jwt.sign(
{ iat: Math.floor(Date.now()/1000)-60,
exp: Math.floor(Date.now()/1000)+600,
iss: "Iv23liAbCdEfGhIjKlMn" },
pk, { algorithm: "RS256" }));'
# Step 2: exchange the JWT for an installation token,
# narrowing to one repo and only the permissions this task needs.
curl -X POST \
-H "Authorization: Bearer $APP_JWT" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
-d '{
"repository_ids": [123456789],
"permissions": { "contents": "read", "pull_requests": "write" }
}' \
https://api.github.com/app/installations/$INSTALLATION_ID/access_tokens
# -> { "token": "ghs_...", "expires_at": "2026-05-30T13:00:00Z", ... }
# Step 3: use the ghs_ token like any bearer.
curl -H "Authorization: Bearer $GHS_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
https://api.github.com/repos/OWNER/REPO/pulls
That pattern, "mint a one-hour token scoped to exactly this task", is what you actually want for an agent. The blast radius is one repo for at most one hour, every API call is attributed to the App in the audit log, and revocation is as simple as suspending the installation.
GitHub has also been iterating on the installation-token format itself. Watch the GitHub changelog for current rollouts that affect token shape and per-request override headers. The 1-hour expiration has been stable across these changes.
The catch: GitHub Apps are infrastructure. You register them in a settings page, generate and store a private key, write JWT-signing code, manage the installation lifecycle, and re-mint tokens before every batch of calls. That is not what a developer wants to do at 11 p.m. to let Claude Code open a PR.
Option 4: OAuth user-to-server tokens. When the agent should act as you
The fourth option is the GitHub App user-to-server flow. The App is still the central identity, but the user authorizes the App against their own account and the resulting token (ghu_…) carries the user's identity. The effective permission set is the intersection of what the App was granted and what the user can do. (Generating a user access token for a GitHub App)
If the App has user-token expiration enabled, user tokens last 8 hours and come with refresh tokens valid for 6 months. API requests are attributed to the user in the audit log and tagged with the App, so reviewers can tell a human click from an agent call. (Authenticating with a GitHub App on behalf of a user, Refreshing user access tokens)
For headless agents (CLI tools, MCP servers, anything without a browser), GitHub supports OAuth Device Flow. The agent shows a short code, the user types it at github.com/login/device, and the agent polls for the token. No client secret is sent.
# 1. Ask GitHub for a device + user code. Only client_id required.
curl -X POST https://github.com/login/device/code \
-H "Accept: application/json" \
-d "client_id=Iv23liAbCdEfGhIjKlMn&scope=repo,read:org"
# -> { "device_code": "...", "user_code": "WDJB-MJHT",
# "verification_uri": "https://github.com/login/device",
# "interval": 5, "expires_in": 900 }
# 2. Show the user_code to the user; they enter it at the verification_uri.
# 3. Poll for the token.
curl -X POST https://github.com/login/oauth/access_token \
-H "Accept: application/json" \
-d "client_id=Iv23liAbCdEfGhIjKlMn&device_code=$DEVICE_CODE&grant_type=urn:ietf:params:oauth:grant-type:device_code"
# -> { "access_token": "gho_...", "token_type": "bearer", "scope": "repo,read:org" }
This flow is the canonical pattern for "agent that runs on a server but represents a real human." If you are building an MCP server or a CLI-driven agent and you want the audit log to show the human, not a bot, this is the path. There is more on the Device Flow in general at headless agent OAuth: the device code flow explained.
Why GitHub's token prefixes matter for leak detection
A small detail that becomes important the first time you almost leak a token. Every GitHub-issued credential has a recognizable prefix: ghp_ (classic PAT), github_pat_ (fine-grained PAT), gho_ (OAuth app), ghu_ (App user-to-server), ghs_ (App installation), ghr_ (App refresh). The prefix scheme exists specifically to drive secret-scanning detection. GitHub has published the rationale and reports a very low false-positive rate. (Behind GitHub's new authentication token formats)
Push protection is on by default for many token types and will block a git push containing a recognized secret. (About push protection, Supported secret scanning patterns)
What you see when your agent leaks a token into a commit and tries to push:
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: - GITHUB PUSH PROTECTION
remote: ===========================================
remote: Resolve the following violations before pushing again
remote: - Push cannot contain secrets
remote: - Secret type: GitHub Personal Access Token
remote: - Location: src/agent/.env:3
Push protection is a real safety net and it catches a lot of leaks in practice. But it is the last line of defense, not the first. An agent that exfiltrates a token through a tool call, a webhook, or a paste into an LLM context never touches a git push. Push protection will not save you from that.
How the four options actually rank for agent use
The honest ranking, given everything above:
- GitHub App with installation tokens, narrowed per request, for any unattended automation that does not need to act as a specific human. One-hour tokens, per-request permission narrowing, clean audit attribution. Worth the setup if you are building anything beyond a one-off script.
- GitHub App with user-to-server tokens, via Device Flow for headless agents, when the agent acts on behalf of a real person and the audit log should reflect that. 8-hour rotation, refresh tokens, identity preserved.
- Fine-grained PAT, scoped to one repo with the minimum permissions, for solo developer use where standing up a full App is overkill. Accept the long-lived secret on disk as a known trade-off.
- Classic PAT, only if a feature you absolutely need has no fine-grained or App equivalent. Treat it as a known accept-the-risk.
Most agent docs stop at "use fine-grained PATs." That is fine advice for a hobbyist with one repo. It is bad advice for anything you want to run for more than a quarter without rotating credentials. If you can spare the afternoon to register an App and write the JWT-mint dance, do it.
The trade-off most agent posts skip
The matrix forces an awkward conclusion. GitHub Apps are the right answer for unattended automation: one-hour rotating tokens, per-request permission narrowing, clean audit attribution. But Apps are infrastructure. The PAT is the developer's compromise: it is pasteable like a classic PAT, but it is now a 366-day broadly-scoped credential in your .env.
You can keep the PAT compromise (paste it into .env and move on) or you can take the App pain (write JWT code, store a private key, manage minting). There is a third option that is worth knowing about, which is to push the credential out of the agent's process entirely. A local credential broker like authsome runs the OAuth flow once per provider, stores the token in an encrypted local SQLite vault, and injects it transparently at a local HTTPS proxy when the agent's request goes out to api.github.com. The agent's environment holds only a placeholder. The real github_pat_… or ghs_… never enters the agent's process memory.
For the multi-account case (one human, personal and work GitHub accounts on the same machine, see also managing multiple GitHub accounts for AI agents), that becomes a config flag instead of a wrapper script:
authsome login github --connection work
authsome login github --connection personal
authsome set-default github work
authsome run -- claude
This does not replace GitHub Apps. If your use case demands one-hour rotating tokens with per-request narrowing, you still register an App. What a broker does is make the user side of OAuth and PAT handling disappear, so you stop choosing between "do the heavy thing" and "leave a long-lived secret in a dotfile."
A short checklist before you ship
A few defaults to set today, in order of how much they reduce blast radius for how little work:
- Audit every classic PAT your agents hold. If any of them have "No expiration" or
reposcope, replace them this week. - If you stay on PATs, make them fine-grained and scope each one to a single repo. Set the shortest expiry you can tolerate.
- If you run an org, require approval for fine-grained PATs accessing org resources. (Org PAT policy)
- For any agent that runs unattended for more than a few hours a week, register a GitHub App. Mint installation tokens per-task and pass
permissionsto narrow them. - Turn on push protection at the org level. (About push protection)
- Keep the real
ghp_,github_pat_,ghs_, andghu_strings out of the agent's process memory if you can. Either mint installation tokens in a small sidecar that the agent calls, or use a local broker that injects credentials at the proxy boundary.
None of this is fancy. All of it is much less work than the post-incident review you avoid by doing it.
The single highest-value change for most agent setups is replacing a ghp_ classic PAT with a github_pat_ fine-grained PAT scoped to one repo. Even if you do nothing else from this post, do that.
Next steps
Quickstart
Install authsome and run an agent under the proxy in under five minutes.
Multiple GitHub accounts
Wire personal and work GitHub accounts into one agent without account confusion.
Prompt injection to exfiltration
The threat model that makes long-lived tokens on disk dangerous.
Headless OAuth
The Device Code flow for agents that have no browser.
Further reading
Running AI agents safely in CI/CD: a 2026 hardening guide
The Comment and Control disclosures in 2025 showed a PR title can be enough to exfiltrate ANTHROPIC_API_KEY and GITHUB_TOKEN from CI-resident AI agents. Here is where the secrets actually live in a GitHub Actions run, the OIDC and egress patterns that shrink the blast radius, and a worked example.
Read postMay 28, 2026Secrets managers vs credential brokers for AI agents: Doppler, Vault, Infisical, and where each fits
Doppler, HashiCorp Vault, and Infisical solve storage, rotation, and access control for AI agents, but they still deliver the raw key into the agent process. Here is where each secrets manager stops, where a credential broker starts, and why you want both.
Read postMay 27, 2026Safe API access for LangChain and LlamaIndex agents
LangChain and LlamaIndex agents load API keys from os.environ, where any prompt injection or compromised tool can read them. Two safe patterns to keep real secrets out of the process.
Read post