In this article
November 4, 2026
November 4, 2026

GitHub App installation tokens are now 520 characters: What to check before November 30

GitHub App installation tokens are now about 520 characters instead of 40. What to audit before the override header is deprecated on November 30, 2026.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

A GitHub App installation access token is the short-lived credential your app presents when it acts as an installation, and it's now about 520 characters long instead of 40. It still starts with ghs_, still carries the same permissions and repository scoping, and still expires after one hour. What changed is the shape of the string, which is the part your code was never supposed to depend on.

If you run a GitHub App, CI automation, or any integration that stores, validates, logs or forwards GitHub tokens, two dates matter. October 2, 2026 is when GitHub declared the new format rolled out. November 30, 2026 is when the temporary header that lets you pick a format stops being respected.

What changed, and what didn't?

GitHub posted the notice on April 24, 2026, began a staged rollout on April 27, and declared it complete on October 2. Newly minted installation tokens now use a stateless ghs_APPID_JWT format by default, which GitHub says makes issuance and validation faster and improves the reliability of its API.

The two GitHub App installation token formats. The legacy format is ghs_ followed by 36 opaque characters, 40 characters in total, with no dots. The stateless format is ghs_, then the app ID, an underscore, and a JWT made of a header, payload and signature separated by two dots, about 520 characters in total and varying with the data inside it.
Same prefix, very different string. Anything that assumed 40 characters, or only letters and digits after ghs_, breaks on the new format.

Two details matter for sizing. The token is about 520 characters, and its length varies with the data stored in it. Don't size anything to exactly 520; leave headroom.

Nearly everything else is the same. Permissions, repository scoping, the one-hour expiration and the installation access token REST endpoint are unchanged. Tokens minted before the change keep working until they expire, and installation tokens still start with ghs_.

The scope is narrower than "GitHub tokens changed." The new format applies to GitHub App installation server-to-server tokens, including the Actions GITHUB_TOKEN, on GitHub Enterprise Cloud and Data Residency. GitHub Enterprise Server isn't affected.

One temptation is worth killing early. The new token contains a JWT, so somebody on your team will want to parse it. GitHub says don't:

The JWT is signed using a GitHub-internal issuer and cannot nor should not be validated by a client app. It contains details about the token such as the target installation, the application, and basic validation details. As with all access tokens, client apps must not take a dependency on the contents of this JWT.

Are ghu_ user tokens affected?

Based on GitHub's current announcements, no. The April notice listed user-to-server tokens used in Copilot code review flows as not in scope yet, and said GitHub would share more details on planned format changes for user-to-server tokens. ghu_ is still the prefix for a user access token for a GitHub App. Read that as "not yet" rather than "never." If your ghs_ handling already treats tokens as opaque strings, your ghu_ code paths can follow the same rule, so audit both on this pass instead of waiting for a second announcement.

Where does a 520-character token actually break?

GitHub published the list, and it doubles as a map of every place a system quietly assumes a credential's shape:

  • Validation that requires tokens to be exactly 40 characters, or patterns written for the legacy format.
  • Database columns, secret stores, or environment variables with a fixed or small maximum length.
  • Proxies, gateways or middleware that truncate or reject long Authorization headers.
  • Logging and secret redaction rules that only match the legacy token pattern.

The first three fail in ways you'll notice: a rejected write, or a 401 you'll first blame on permissions. The fourth one fails silently and writes live credentials into your logs in plaintext, where they stay valid for up to an hour. Check that one first.

How do you check each one?

Validation and redaction. GitHub's April notice named ghs_[A-Za-z0-9]{36} as a pattern that may not match the new tokens. Its recommended replacement, published in the May 15 changelog and updated May 26, matches both the legacy and stateless formats:

  
// GitHub's recommended pattern: ghs_[A-Za-z0-9\.\-_]{36,}
// Open-ended length, and it allows the dots and underscores a JWT brings.
const GHS_TOKEN = String.raw`ghs_[A-Za-z0-9\.\-_]{36,}`;

const isInstallationToken = new RegExp(`^${GHS_TOKEN}$`);
const findInstallationTokens = new RegExp(GHS_TOKEN, "g");

export function looksLikeInstallationToken(value: string): boolean {
  return isInstallationToken.test(value.trim());
}

export function redactTokens(line: string): string {
  return line.replace(findInstallationTokens, "ghs_[REDACTED]");
}
  

Test redactTokens against a real stateless token. A fixture holding a 40-character token from before the rollout passes under both the old pattern and the new one, which tells you nothing.

Storage. Database columns for token storage need room for tokens of about 520 characters that vary in length, and so do any header settings on the way in. Prefer a variable-length text column over a tight char or varchar(255). Secret stores and environment variables get the same audit.

Proxies and gateways. Check the maximum header size on every hop that sees an Authorization header, including load balancers, WAFs, API gateways, and anything that writes request logs. A configuration review isn't enough here. Send a real stateless token end to end and confirm what arrives at the origin.

Minting a stateless token on demand. Until November 30 you can force either format per request by setting X-GitHub-Stateless-S2S-Token on POST /app/installations/:installation_id/access_tokens. The value enabled returns a stateless token, disabled returns a classic opaque one, and an absent header gives you normal rollout behavior. Any other value, including true or 1, is silently ignored.

  
curl --request POST \
  --url "https://api.github.com/app/installations/INSTALLATION_ID/access_tokens" \
  --header "Accept: application/vnd.github+json" \
  --header "Authorization: Bearer YOUR_APP_JWT" \
  --header "X-GitHub-Stateless-S2S-Token: enabled"
  

To confirm which format came back, count the dots after the ghs_ prefix. A stateless token is a JWT and contains two dots. A stateful token is a short opaque string with none.

What happens to the override header on November 30?

GitHub stops respecting it, and all eligible apps always receive stateless tokens. GitHub's instruction is direct: "Once you've validated your apps and workflows with both token formats, remove the header from your production code before November 30, 2026."

The enabled value quietly becomes a no-op. The one to hunt for is disabled, the opt-out some teams used to buy time during the rollout. Anything still sending disabled on November 30 gets a 520-character token whether or not the code behind it is ready. Grep for the header name across application code, CI config, and any shared HTTP client library, including services that sit outside your usual deploy cadence.

Which places should you check first?

Where to look What fails The fix
Token validation and introspection code A pattern like ghs_[A-Za-z0-9]{36} rejects a valid token Match ghs_[A-Za-z0-9\.\-_]{36,}, or check only the prefix
Log and secret redaction rules New-format tokens are written to logs in plaintext because the legacy pattern misses them Same pattern, applied to the redaction filter, then tested with a real stateless token
Database columns, secret stores, environment variables Writes are truncated or rejected on a fixed or small maximum length Size for tokens of about 520 characters with headroom, and prefer variable-length storage
Proxies, gateways, middleware Long Authorization headers get truncated or rejected, surfacing as confusing 401s Raise header limits and verify with an end-to-end request carrying a stateless token
Code that parses the token Logic reading claims out of the embedded JWT Treat the token as an opaque string; the JWT is signed by a GitHub-internal issuer and isn't yours to validate
Calls that set X-GitHub-Stateless-S2S-Token After November 30 the header is ignored, including disabled Remove it from production code before November 30, 2026

What should you do this week?

The fix GitHub asks for fits in one sentence: confirm that every system handling installation tokens treats them as opaque strings. Every row in the table above is a violation of that one rule, which is why a single discipline retires all of them at once. It costs almost nothing to follow before a format change and a lot to retrofit during one.

Start with the logging and redaction configs. Grep them for the legacy 40-character pattern, because that's the failure that writes live credentials into plaintext logs, where they stay usable for up to an hour, and it's the only item on the list that won't page you. Then go find your ghs_ column widths. For another recent case of a platform changing the rules under running automation, see our write-up on GitLab's rate limit changes.

Further reading