Benefits of the New GitHub App Token Format for Automation
GitHub has finished rolling out the new GitHub App token format. Installation tokens now look like ghs_APPID_JWT and run about 520 characters instead of 40. Your first reaction was probably "great, something else to break." Fair, but there is a short list of things you get in return, and most of them cost nothing.
Below are four benefits, each with a way to check it yourself, plus an honest note on when you can skip the whole thing.
What changed in the GitHub App token format
Per GitHub's rollout announcement, newly minted installation tokens are stateless by default. They keep the ghs_ prefix but carry a signed JWT (JSON Web Token, a signed bundle of claims) inside. GitHub says the JWT holds details like the installation, the app and basic validation data, and that apps must not depend on its contents.
Benefit 1: Faster issuance and validation, and a steadier API
GitHub says the change "makes token issuance and validation faster and improves the reliability of the GitHub API." The earlier notice adds that it improves issuance performance under increased load. GitHub published no numbers, so I won't invent any.
While the override header still works, you can compare the two formats on your own app:
# APP_JWT is your app's signed JWT, not an installation token
for mode in disabled enabled; do
echo "== $mode"
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w '%{time_total}s\n' \
--request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer $APP_JWT" \
--header "X-GitHub-Stateless-S2S-Token: $mode" \
"https://api.github.com/app/installations/<installation-id>/access_tokens"
done
done
Per the override header notice, enabled forces the stateless format and disabled forces the classic one. This mints ten real tokens, so don't run it on every push. Five samples is a sanity check that nothing got slower, not a benchmark. GitHub's claim is about behavior under load, which a loop from one machine won't show.
Benefit 2: A drop-in change for permissions and scoping
The endpoint, the one-hour expiry, the permissions and the repository scoping are all unchanged. Tokens issued before the switch keep working until they expire. Your least-privilege setup survives as is:
curl -s --request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer $APP_JWT" \
"https://api.github.com/app/installations/<installation-id>/access_tokens" \
--data '{"repositories":["<repo-name>"],"permissions":{"contents":"read","issues":"write"}}' \
| jq '{expires_at, permissions, repository_selection, token_length: (.token | length)}'
The request body (repositories, permissions) is the one in the REST API docs, and the response keeps the same fields. Only token_length moves, to roughly 520. No new scopes to review, no security sign-off to redo.
Benefit 3: GitHub App token validation is a one-line check
Telling the formats apart is easy. Stateless tokens are ghs_ JWTs with two dots. Classic tokens are short opaque strings with no dots. That makes a mixed fleet easy to watch in your logs:
import re
# GitHub's recommended pattern for ghs_ tokens (revised May 26, 2026)
GHS_TOKEN = re.compile(r"ghs_[A-Za-z0-9.\-_]{36,}")
def token_kind(token: str) -> str:
if not GHS_TOKEN.fullmatch(token):
raise ValueError("not a ghs_ installation token")
return "stateless" if token.count(".") == 2 else "stateful"
log.info("github token kind=%s", token_kind(token))
Use this for logs and metrics only. Don't decode the JWT or branch your auth logic on it, because GitHub says clients shouldn't validate it. GitHub revised its regex advice once already, so copy the current pattern from the changelog, not from a blog post (including this one).
Benefit 4: GitHub App installation token length assumptions go away
GitHub's checklist doubles as a bug hunt you should have done anyway:
- Validation that expects exactly 40 characters, such as
ghs_[A-Za-z0-9]{36}. - Database columns, secret stores or environment variables with small limits. GitHub says to fit at least 520 characters, and the length varies with the data inside, so leave headroom.
- Proxies, gateways or middleware that truncate or reject long
Authorizationheaders. - Logging and secret-redaction rules that only match the legacy pattern.
-- Postgres: no length limit to outgrow
ALTER TABLE github_installations
ALTER COLUMN access_token TYPE text;
While you're in there, delete the X-GitHub-Stateless-S2S-Token header from production code. GitHub deprecates it on November 30, 2026, after which it ignores the header and eligible apps always get stateless tokens. Removing it now leaves you with one code path instead of two.
When it's not worth the effort
If your code already treats tokens as opaque strings and passes them straight to the API, there is nothing to adopt. The benefits arrive on their own, so don't start a migration project.
Low-volume automation won't feel faster issuance either: one token per hour per installation is not a hot path. GitHub says GitHub Enterprise Server isn't affected. And the format isn't optional once the header is deprecated, so the real question is only how tidy you want your cleanup to be.
If your coding agents or CI bots authenticate as a GitHub App, the trade is a faster, steadier token endpoint in exchange for an afternoon spent removing 40-character assumptions.