GitHub has finished rolling out stateless GitHub App installation tokens. The official changelog says newly minted installation tokens still start with `ghs_`, but are now about 520 characters long instead of 40. The permissions, repository scope, one-hour lifetime, and token endpoint are unchanged.
That sounds like an internal implementation detail, until an integration rejects the token because a database column is too short, a proxy truncates the Authorization header, or a redaction rule only recognizes the old shape. The fix is not to decode the new value. Treat installation tokens as opaque strings everywhere they travel.
What changed and what did not
The token format changed on the value returned by the GitHub App installation access-token flow. New tokens use the stateless format by default. Existing tokens keep working until they expire, so a deployment can appear healthy while a later token refresh fails.
- The prefix remains `ghs_`. Do not use the prefix as a complete validation rule.
- The token can be much longer than the legacy 40-character value.
- Permissions, repository restrictions, and the one-hour expiration still apply.
- The REST endpoint for generating an installation token is unchanged.
Find the assumptions that break
Search the code and infrastructure around your GitHub App, not just the function that creates the token. Check these places first:
- Validation code that checks `token.length === 40` or uses a regular expression for the old shape.
- Database columns, secret stores, cache schemas, or environment-variable limits sized for 40 characters.
- Reverse proxies, API gateways, load balancers, or WAF rules that limit or rewrite long Authorization headers.
- Secret scanners and log filters that redact only a 40-character token.
- Tests and fixtures that hard-code the legacy token length.
Make the token opaque in application code
Your app should receive the token as a string, pass it to the HTTP client, and discard or replace it when it expires. Do not split it on underscores, base64-decode it, or use a token length as a proxy for validity. The GitHub documentation describes the token as an installation credential with permissions, repository access, and an expiration time. Those are the properties your integration should model.
const token = await createInstallationToken(); const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });If you store a token briefly for reuse, size the field for a normal string value rather than the current observed length. Better still, store the expiration alongside it and refresh through the app-authentication path instead of trying to infer freshness from the token text.
Test the full Authorization header path
A local unit test proves that your code can hold a long string. It does not prove that the production path can carry it. Run an authenticated request from the same worker, container, proxy route, and gateway that your app uses in production. Check the outgoing header length at a safe boundary without writing the secret itself to logs.
- Mint a new installation token in a non-production test environment.
- Call a harmless GitHub API endpoint using the same network path as the real integration.
- Confirm the request reaches GitHub without truncation or a gateway rejection.
- Repeat after the token refresh path runs, because old cached tokens can hide the new format.
Remove the temporary rollout header before November 30
GitHub introduced `X-GitHub-Stateless-S2S-Token` as a temporary way to opt into the new format during the staged rollout. GitHub says that header will be deprecated on November 30, 2026. After that date it will no longer be respected, and eligible apps will always receive stateless tokens. If you still set it, remove it from production code after testing both formats.
Update secret handling at the same time
Longer tokens are a good reason to test your secret controls, but they are not a reason to print a token to inspect it. Verify that request logs, exception reports, tracing spans, queue payloads, and support bundles redact the complete Authorization header. Keep the app's private key and installation token separate in both permissions and retention policy.
Migration checklist
- Remove fixed 40-character checks and token-specific parsing.
- Check database, cache, secret-store, gateway, and header limits.
- Test a real token refresh through the production-shaped network path.
- Review complete-header redaction and secret retention.
- Remove the temporary stateless-token header before November 30, 2026.
The safe mental model is simple: an installation token is a short-lived opaque credential, not a 40-character identifier. Make that true in your types, storage, network path, tests, and logs, and the format change should stay boring.
