A GitHub App installation token used to be 40 characters. GitHub's 2 October changelog post says "Installation tokens still start with the ghs_ prefix, but they're now about 520 characters long instead of 40." Anything in your stack that assumed the short form (a validator, a database column, a proxy, a log scrubber) can break at the next mint. The date to plan around is 30 November 2026, when GitHub stops honouring the header that let apps opt out of the long token.
What changed in the token
The new format is called stateless and looks like ghs_APPID_JWT. The docs page on generating an installation token says GitHub began a staged rollout of it on 27 April 2026 to "all newly minted GitHub App installation tokens". The 2 October post says GitHub has completed that rollout.
GitHub left the rest alone. Per the 2 October post: "Token permissions, repository scoping, the one-hour expiration, and the installation access token REST API endpoint are unchanged." The docs still say the token expires after 1 hour, and the 15 May post says existing tokens remain valid until expiration.
GitHub's three posts word the scope differently, so read each on its own terms:
- GitHub's 24 April notice about the upcoming token format: "This change applies to GitHub Enterprise Cloud and Data Residency environments. GitHub Enterprise Server isn't impacted by this change." The same notice says the rollout includes the Actions
GITHUB_TOKEN. - 15 May post: the per-request header applies to GitHub Enterprise Cloud and Data Residency only.
- 2 October post: describes "all newly minted GitHub App installation tokens" and doesn't say anything about Enterprise Server either way.
If you run Enterprise Server, the April notice is the one to read. Don't treat the October wording as widening it.
Why 30 November matters
The format change isn't what happens on 30 November. It has already happened. What ends is the temporary X-GitHub-Stateless-S2S-Token request header. The 2 October post says it "will be deprecated on November 30, 2026. After that date, GitHub will no longer respect the header, and all eligible apps will always receive stateless tokens."
The 15 May override-header post defines the header's values: enabled returns a stateless token, disabled returns the old stateful one, no header means the standard rollout, and any other value is silently ignored.
So who is exposed on 30 November? Apps that sent disabled as a workaround. They get the long token from that date. An app that never sent the header gets stateless tokens by default, per the 2 October post: "By default, all newly minted GitHub App installation tokens will be in the stateless ghs_APPID_JWT format." Your hidden problem is a quiet one: a bug that exists today but surfaces only in the code path that runs rarely.
How to audit your GitHub App integrations
Five steps, in this order. The checks mirror the failure classes GitHub lists in its October post, with a way to find each one in your own code. All code below is illustrative glue, not from GitHub's docs unless stated.
1. List every place a token is minted or handled
Write down each GitHub App you run, then each system that touches its tokens: the service that signs the app JWT and calls the endpoint, bots, CI glue, secret stores, caches, log pipelines. Ask each owner where the token lands after minting. The "internal bot nobody has touched since 2023" is the usual surprise. If your CI hands out installation tokens, scoped, short-lived tokens in CI covers why that's the right pattern, and it makes this inventory short.
2. Find length and format checks
GitHub's April notice says: "Your apps do not take a dependency on access tokens being a certain length." The October post names validation that requires exactly 40 characters, or legacy patterns, as a failure class. Search your code for it. This grep is a starting point. It assumes a POSIX-style shell, the include list is partial (add your other languages), it will produce false positives, and it can miss checks written in other ways:
grep -rnE 'ghs_|\{(36|40)\}|(==|!=|<=?|>=?|-eq|-ne) ?40|(varchar|char|String)\(40\)' \
--include='*.py' --include='*.js' --include='*.ts' \
--include='*.go' --include='*.rb' --include='*.java' .It won't see a length held in a named constant, for one. The anti-pattern GitHub names in the April notice is ghs_[A-Za-z0-9]{36}. The pattern GitHub recommends in the 15 May post "to match both new and current format tokens" is:
ghs_[A-Za-z0-9\.\-_]{36,}It matches the four-character prefix plus 36 or more characters, so it accepts the 40-character legacy token and the long form, and it allows the dots the new format contains. GitHub's pattern is written for PCRE-style engines (JavaScript, Python, Go, grep -P). For grep -E or sed -E, use ghs_[A-Za-z0-9._-]{36,}. Both are unanchored; a validator needs ^ and $.
3. Find storage with a size limit
The April notice says: "Any database columns for access tokens can fit at least a 520 character string." Note the "at least". GitHub describes the length as varying with the data stored in the token and states no maximum, so any hard cap at exactly 520 is a judgment call that may bite later. Leaving headroom, or using an unbounded type, is the safer call (that's our judgment, not GitHub's).
On Postgres, this query lists character columns with "token" in the name that are too short:
SELECT table_name, column_name, character_maximum_length
FROM information_schema.columns
WHERE column_name ILIKE '%token%'
AND data_type IN ('character varying', 'character')
AND table_schema NOT IN ('pg_catalog', 'information_schema')
AND character_maximum_length < 520;For each hit, a fix looks like this, with your own table and column names in place of the placeholders:
ALTER TABLE your_table ALTER COLUMN your_column TYPE text;That ALTER ... TYPE text rewrites the table and fails if a view depends on the column, so schedule it.
GitHub's October list also names secret stores and environment variables "with a fixed or small maximum length". Check the limits on the ones you own.
4. Check proxies, gateways and logging
The October post flags systems that "truncate or reject long Authorization headers". A token that fits your database can still die in an API gateway, a corporate proxy or a sidecar in front of the service that uses it. No default limits are quoted here because GitHub doesn't give any: read the configuration of each hop and test it.
The test below mints a token with the header set to enabled and prints only its length and dot count, never the token, and fails loudly if no token comes back. APP_JWT, INSTALLATION_ID and GITHUB_API are your own variables, and jq is assumed to be installed. The headers and endpoint are the documented ones.
curl -sS -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $APP_JWT" \
-H "X-GitHub-Api-Version: 2026-03-10" \
-H "X-GitHub-Stateless-S2S-Token: enabled" \
"$GITHUB_API/app/installations/$INSTALLATION_ID/access_tokens" \
| jq -r '.token // empty' \
| awk 'NF { n = length($0); d = gsub(/\./, ""); print "length=" n, "dots=" d; found = 1 }
END { if (!found) { print "no token in response (check APP_JWT, INSTALLATION_ID, GITHUB_API)" > "/dev/stderr"; exit 1 } }'Only count the token field of the response. Don't paste a real token into a ticket or chat. Per the 15 May post, a stateless token has two dots after the ghs_ prefix and a stateful one has none, so the output here (illustrative, not real; the actual length varies) is:
length=520 dots=2Then send a real long token through the same path your app uses and confirm the downstream service accepts it.
5. Update redaction, then test with both header values
Secret scanners and log redactors built around the 40-character form will miss a 520-character token, or redact only the first 40 characters and leave the rest in your logs. Switch them to the pattern from step 2, in the PCRE form or the grep -E form to suit the engine, and anchor it if the tool validates a whole string.
Then run the real flow three ways: with enabled, with disabled (so you know what the old path looked like), and with the header removed. The header test only works while GitHub still honours it, which is why this audit has a deadline. After 30 November the header does nothing, and only the long token is left to test.
What this means for your platform or security owner
Assign one person now, with a date before 30 November, to audit every GitHub App integration. Include internal bots, because those are the integrations most likely to lack monitoring (our judgment). The questions: does anything pin disabled, and if so, who removes it? Is any column, secret store or environment variable too small? Does any proxy trim Authorization headers? Do your redaction rules cover the long form?
Because the token expires after 1 hour, a failure won't be a one-off. It appears at every mint until someone fixes it.
This is one of several GitHub changes with dates on them. The macos-14 runner retirement and the self-hosted runner version enforcement sit on the same calendar. For a running list of what needs an owner, start with Platform and language release notes: what breaks and what to fix.
FAQ
Do I need to change anything if I never used the header?
Per the 2 October post, stateless is the default for all newly minted tokens, so the long token is already what you're minting. The deprecation changes nothing for you. The audit still applies, because the format change has already reached you.
How long is the new token?
GitHub says "about 520 characters long instead of 40". The April notice says the length varies with the data stored in the token and promises only that columns can fit "at least" 520 characters. GitHub states no maximum, so size storage with headroom.
Is the one-hour expiry changed?
No. The 2 October post lists "the one-hour expiration" among the unchanged items, and the docs say the token expires after 1 hour.
Does this affect GitHub Enterprise Server?
The 24 April notice says "GitHub Enterprise Server isn't impacted by this change." The 2 October post doesn't address it either way, so rely on the April wording and check GitHub's posts before you upgrade or plan around it.
