
GitHub App tokens went from 40 to 520 characters
Quick answer: As of 2 October 2026 every newly minted GitHub App installation token is a stateless ghs_APPID_JWT string of roughly 520 characters instead of a 40-character opaque one. The prefix, the permissions, the one-hour expiry and the REST endpoint are all unchanged — so nothing in your auth logic breaks. What breaks is everything that assumed the length: a VARCHAR(40) column, a regex that redacts ghs_[A-Za-z0-9]{36}, a proxy with a header size cap. Fix those, then delete the X-GitHub-Stateless-S2S-Token header before it is deprecated on 30 November 2026.
This is the rare breaking change that breaks nothing in the thing it changes. GitHub did not alter how installation tokens authenticate, what they can reach, or how long they live. It made them thirteen times longer, and that turns out to be enough.
The 2 October changelog entry is three paragraphs long and reads like housekeeping. It is the end of a rollout that started on 27 April 2026 and has now reached every integration. If you build on the GitHub API and you have not audited for this, you are already running on the new format.
What the new token actually is
The old installation token was an opaque bearer string: 40 characters, ghs_ plus 36 of random, meaningful only to GitHub's token service, which had to look it up on every request. That lookup is the "stateful" part, and removing it is the point of the exercise.
The replacement is described in GitHub's own docs as the format ghs_APPID_JWT. Three things follow from that shape. The prefix is still ghs_, so prefix-based detection keeps working. The App ID is now embedded in the token itself, in clear text. And the tail is a JWT, which means the token contains two dots and base64url segments — paste one into a JWT decoder and the header and payload come apart like any other JWT.
That last property is the one people misread. The JWT is readable, but it is not yours to verify. As the maintainers of one secret-scanning tool put it while adding support for the format, "The JWT is signed using a GitHub-internal issuer and cannot nor should not be validated by a client app." Decode it for debugging. Do not build a signature check.
Stateful vs stateless, side by side
| Property | Classic (pre-rollout) | Stateless (now default) |
|---|---|---|
| Length | 40 characters | ~520 characters |
| Shape | Opaque, no dots | ghs_APPID_JWT, two dots |
| Prefix | ghs_ | ghs_ (unchanged) |
| Expiry | 1 hour | 1 hour (unchanged) |
| Permissions & repo scoping | Per installation | Per installation (unchanged) |
| Mint endpoint | Installation access token REST API | Same endpoint |
Read that table as a list of things you do not have to touch. GitHub's wording is explicit: "Token permissions, repository scoping, the one-hour expiration, and the installation access token REST API endpoint are unchanged." Revocation still exists too — the REST reference still documents revoking the token you are currently authenticating with, and still says that once revoked "the token is invalidated and cannot be used."
The four places it actually bites
GitHub names four audit targets, and they are worth taking literally because each one fails in a different, confusing way.
1. Validation logic that enforces 40 characters
Anything of the form len(token) == 40 or ^ghs_[A-Za-z0-9]{36}$ now rejects every token GitHub hands you. GitHub's docs page says it plainly: "If your application expects or relies on installation tokens being exactly 40 characters long, it may not handle this new token format correctly." If you wrote such a pattern, run the new shape against it in a regex tester before you trust your fix — the dots and the embedded App ID both break naive character classes.
2. Storage that is too narrow
A VARCHAR(40) column, a fixed-size struct field, a Kubernetes secret key with a length assumption downstream. Depending on your database this either errors loudly or silently truncates, and a silently truncated bearer token produces a 401 that looks like an expiry bug. This is the failure mode most likely to waste an afternoon.
3. Proxies and gateways that cap header size
An Authorization: Bearer header grew by about 480 bytes. On its own that clears every sane default, but it does not travel alone — if you are already forwarding a long cookie, a trace context and a few custom headers through a reverse proxy with a tight buffer, this is the increment that tips you over. The symptom is a 400 or 431 from your own infrastructure, not from GitHub.
4. Log redaction rules
This is the one with teeth. If your scrubber matches a fixed-length ghs_ pattern, it no longer matches, and live installation tokens start landing in your logs in plain text. They expire in an hour, which limits the blast radius but does not eliminate it. Rewrite the rule to match the prefix and run to a whitespace boundary rather than counting characters.
This already broke real tooling
The clearest evidence that length-based assumptions are everywhere is in the secret-scanning ecosystem, which exists to pattern-match credentials. Kingfisher, MongoDB's secret detection tool, carried an open issue from the April announcement through to a release that handled the new shape, and the discussion is instructive: the maintainers concluded they needed additional rules rather than a replacement pattern, because during a staged rollout both formats are live at once.
That dual-format window is now closed for new tokens, but if you cache or archive tokens anywhere, your corpus still contains both. Any detector you own needs to keep matching the old shape for as long as your retention does. The same logic applies to every scanner in your CI and deployment pipeline, not just the one GitHub ships.
The override header has a deadline
Since 15 May 2026 the installation-token endpoint has accepted a temporary request header, X-GitHub-Stateless-S2S-Token, that overrides GitHub's rollout decision per request. enabled forces a stateless token regardless of where your integration sits in the rollout; disabled forces a classic opaque one even if you are already migrated. Other values — true, false, 1, 0 — are ignored, which is a quiet trap for anyone who guessed at the vocabulary.
That header is deprecated on 30 November 2026. After that date GitHub discontinues support and every eligible app receives stateless tokens only. If you shipped disabled as a stopgap during the rollout, you have about eight weeks to remove it, and the failure mode when the header stops working is that the behaviour you pinned against silently reverts.
The docs do not carry the numbers
Worth knowing if you are the person who checks the reference rather than the changelog. GitHub's page on generating an installation access token does name the new ghs_APPID_JWT format and does warn about the 40-character assumption — but it does not state the ~520-character length, and it does not mention the 30 November 2026 header deprecation. Both of those live only in changelog entries. The REST reference for the installation endpoints says nothing about format at all.
This is a pattern we keep running into on this blog: vendor reference docs lag vendor changelogs, and the number you need to size a column with is in the changelog. The same split showed up in npm's move to stage-only publishing tokens. Subscribe to the changelog; do not assume the reference page is complete.
A 20-minute audit
Grep your codebase for the literal 40 near anything token-shaped, for ghs_, and for {36}. Grep your migrations for VARCHAR(40). Then check your log scrubber's rules and your proxy's header buffer settings. Official clients pass the token through as an opaque string, so the exposure is concentrated in the code you wrote around them: the glue that persists a token, the proxy it travels through, and the observability stack that logs it. Those are exactly the four targets GitHub names.
Then do the thing most teams skip: mint a token in a scratch environment and look at it. Decode the JWT tail with a base64 decoder to confirm what is actually in the payload for your app, so you know what would be exposed if one did leak into a log. Tokens that embed an App ID in clear text are not secret-free — they tell a reader which app to go after.
If you are picking auth tooling around GitHub Apps rather than fixing existing code, the authentication and identity category on TechLogHub lists the token and secret-management products that handle this layer, and API and backend tools covers the gateway side.
FAQ
Do I need to change how my app authenticates?
No. You still exchange an app JWT for an installation token at the same REST endpoint and send it as a bearer token. GitHub states that permissions, repository scoping, the one-hour expiry and the endpoint are all unchanged.
Exactly how long is the new token?
GitHub says approximately 520 characters. It is not a fixed length — the App ID segment varies and the JWT payload can change — so size storage generously rather than to 520 exactly. A 1,024-character column is a reasonable floor.
Can I validate the JWT signature myself?
No. It is signed by a GitHub-internal issuer with no published verification key, and tooling maintainers who looked at it concluded it "cannot nor should not be validated by a client app." Treat it as an opaque bearer token that happens to be decodable.
Can I still get an old-format token?
Until 30 November 2026, yes — send X-GitHub-Stateless-S2S-Token: disabled. After that date the header is deprecated and all eligible apps receive stateless tokens only. Use the window to fix the assumption, not to extend it.
Does revocation still work?
Yes. The REST reference still documents revoking the installation token you are authenticating with, and still states that a revoked token is invalidated and cannot be used.
When did this rollout start?
GitHub announced the format in April 2026 and began a staged rollout on 27 April 2026. The 2 October 2026 changelog entry declares that rollout complete for all newly minted installation tokens.
The cheapest breaking changes to miss are the ones that change a size rather than a behaviour. Grep for the number, not the feature.

