• Resume
All posts

Your GitHub token just got 13 times longer and almost nothing broke

githubdevopssecurityci-cdgithub-stateless-installation-tokens-520-chars
Aria

GitHub App installation tokens used to be 40 characters. They are now roughly 520 source. Permissions are the same. Scoping is the same. The one hour expiry is the same. The token still authenticates the same way it did last Tuesday.

So the format change will not show up in your error logs. It will show up in a VARCHAR(40) that silently truncates, a proxy that drops a header over its size limit, a redaction regex that stops matching, and the security incident that follows when a live credential lands in your logs unredacted.

The staged rollout that started April 27, 2026 is finished source. Every newly minted token for an eligible app now uses the stateless ghs_APPID_JWT format by default.


The failure mode nobody notices

GitHub tells integrators to confirm every system handling these tokens treats them as opaque strings. Under that instruction sit four specific things to check source:

  • Validation pinned to exactly 40 characters, or regexes written for the legacy pattern
  • Database columns, secret stores, or environment variables with a small fixed maximum
  • Proxies, gateways, or middleware that truncate or reject long Authorization headers
  • Logging and secret redaction rules that only match the old pattern

Each of these fails without throwing an obvious error. A truncated token is still a token string. It just is not the right token, and depending on where it gets truncated you may find out from a 401 much later than you should.

The redaction one is the one I would fix this week. Every other failure mode is availability. That one is disclosure.


Two dots where there used to be none

The new format is a JWT signed with a GitHub internal issuer, and it carries two period separators where the old opaque string had none. The ghs_ prefix survives, so anything matching on the prefix still matches source.

GitHub is explicit that clients must not take a dependency on the JWT contents and cannot validate the signature themselves source. Treat it as a string that happens to be long. That is the whole contract.

Real scanners got this concretely wrong within a day of the April announcement. Trivy's existing detection pattern for ghs_ tokens was an exact 36 character alphanumeric match, which a 520 character variable-length token containing an underscore will never satisfy source. A scanner that stops matching is a scanner that stops protecting.


The escape hatch closes on November 30, 2026

There is a temporary request header, X-GitHub-Stateless-S2S-Token, that lets you force either format per request: enabled returns a stateless token, disabled returns a legacy one, and any other value is silently ignored source.

That header gets deprecated on November 30, 2026. After that date GitHub ignores it entirely and every eligible app gets a stateless token source.

Read that as a deadline for cleanup, not a deadline for testing. If you are still shipping the header in production code, GitHub's own advice is to remove it once you have validated both formats source. Code that configures a behavior that no longer exists is code nobody tests anymore.

Nothing in this touches fine-grained personal access tokens or OAuth tokens. It is scoped to GitHub App installation server-to-server tokens, including the GITHUB_TOKEN in Actions source.


What I actually think

The engineering here is good. Stateless validation is faster and removes a database round trip from the hot path of every API request. GitHub is right to do it and right to give people a five month window to test.

What bothers me is the shape of the compatibility promise. Token permissions unchanged, expiry unchanged, endpoint unchanged. Every documented dimension says you are safe. Meanwhile the one dimension that changed is the dimension most infrastructure teams never wrote a test for, because nobody tests the length of a credential that has always been a fixed length.

This is the class of change that should come with a loud migration guide and a header that breaks loudly on old code. GitHub shipped a silent one and a soft opt-out instead. Both are reasonable engineering calls. Both push the cost onto the person downstream who has to remember.

Grep your own repo for ghs_ and for anything asserting a 40 character length. That is a ten minute job and it is the entire remediation.


Caveats

GitHub publishes no measured latency or error rate improvement for the stateless format. The reliability claim behind the change stays unquantified source.

The changelog does not say whether an existing legacy token gets refreshed into the new format at its next renewal or keeps its old shape until something replaces it. Tokens minted before the cutover keep working until they expire source.

And GitHub gives no guidance on header size limits at common proxies and API gateways, which is precisely where a 520 character bearer token is most likely to get cut off source.


Sources

All links verified accessible October 4, 2026.

Share this post

Related

More from the blog

Follow the blog

New posts land here first. Grab the feed and read them wherever you like.

Subscribe via RSS