🚀 New: chi (χ) — an open-source autoresearch harness for fleets of LLM coding agents. Read the announcement.

GitHub App Installation Tokens Are Now About 520 Characters. The Bugs Will Be in Your Redaction Rules

GitHub finished moving App installation tokens from 40 characters to a roughly 520 character ghs_APPID_JWT format, and the X-GitHub-Stateless-S2S-Token override header stops working on November 30, 2026. What changed, which code paths break quietly, and what to test before the rollback lever disappears.

Contents

If a system of yours validates, stores, forwards or redacts GitHub App installation tokens, the format changed under it this year, and on November 30, 2026 the last switch for undoing that goes away. GitHub’s changelog for October 2 says the staged rollout that began on April 27 is complete. Newly minted installation tokens are now ghs_APPID_JWT, about 520 characters long instead of 40.

The part worth a closer look is not minting. Minting returns the token and your client carries on. The breakage lives in every place that holds the token afterwards, and the nastiest case does not throw.

What changed and what did not

According to GitHub’s April notice, the new token is stateless. The JWT part is signed with a GitHub-internal issuer and carries details such as the target installation, the app and basic validation data. GitHub says client apps cannot and should not validate it, and must not depend on its contents. Its stated reason is issuance performance and reliability under load. The notices do not describe the server design. A self-contained signed token is the usual way to avoid a per-token lookup, but that is my inference, not something GitHub wrote.

The October changelog says what stays the same: permissions, repository scoping, the one-hour expiry, and the POST /app/installations/:installation_id/access_tokens endpoint. Tokens minted before the change keep working until they expire, which is at most an hour. The format is also still prefixed ghs_.

One detail explains why old validators fail. The legacy token was 40 characters, which is the ghs_ prefix plus 36 alphanumerics. A pattern like ghs_[A-Za-z0-9]{36}, which GitHub’s April notice names as the kind of regex that may stop matching, fails twice on the new format. It is too short a match for 520 characters, and it has no way to match the underscore after the app ID or the dots in the JWT.

I ran GitHub’s recommended pattern and the old one against two synthetic strings I built, a 40-character legacy-shaped token and a JWT-shaped one with two dots. These are not real tokens, so this only shows the pattern behavior, not GitHub’s actual encoding:

import re
recommended = re.compile(r'ghs_[A-Za-z0-9\.\-_]{36,}')   # from GitHub's May 15 changelog
legacy_only = re.compile(r'ghs_[A-Za-z0-9]{36}')
# legacy-shaped (40 chars):  recommended matches, legacy_only matches
# JWT-shaped (2 dots):       recommended matches, legacy_only does not

The failures that make noise and the one that does not

GitHub’s October post lists four places to look. Two of them are loud:

  • Validation that requires exactly 40 characters. This rejects the token at your boundary.
  • A database column, secret store or environment variable with a small maximum length. This truncates or errors, and GitHub’s earlier notice asks for room for at least 520 characters.

A third is mixed: proxies, gateways or middleware that truncate or reject long Authorization headers. You will see a 4xx or a 5xx from somewhere in the path, though the cause may be several hops away from your code.

The fourth is the one I would audit first: logging and secret-redaction rules that only match the legacy pattern. A redaction rule that stops matching does not fail a request or page anyone. It lets a live credential, valid for up to an hour, into your logs. The same holds for secret scanners. The Trivy project’s tracking issue, aquasecurity/trivy#10591, opened the day after the rollout started, says its detection regex “will silently miss all tokens in the new format”. Its first suggested character class had no dot in it, while GitHub’s format has two. A later comment on the issue points to GitHub’s recommended pattern, and the issue was closed as completed in a PR on June 16. I did not read that PR, so I cannot say which regex shipped.

The takeaway is not about Trivy. It is that a pattern written from a sample token is a bet on the token’s shape, and GitHub has now told you the shape varies “based on the data stored within it”.

Why November 30 matters more than it looks

The temporary request header, introduced on May 15, is X-GitHub-Stateless-S2S-Token on the token creation endpoint. The value enabled forces a stateless token, and disabled forces a classic one even if your integration is already in the rollout. Any other value is silently ignored, so true or 1 do nothing. Per the October post, GitHub will stop respecting the header on November 30, 2026, and all eligible apps will always get stateless tokens.

That makes the header a rollback lever with an expiry date. If any of your code or workflows sets disabled, they have been receiving old-format tokens since the rollout reached them, and every system downstream of them has not yet seen a long token in production. After November 30 they all will, at once.

The test is cheap to run before then. Request a token with the header set to enabled against a non-production installation, using the same call as the docs plus one header:

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

GitHub’s May post suggests checking the format by counting dots after ghs_: two means stateless, none means the opaque form. Then push that token through the paths that matter, not just the API call: your secret store write, your log pipeline with the token present in a header, your scanner, and whatever proxy sits in front of your own services.

Scope caveats

The April and May notices scoped the change to GitHub Enterprise Cloud and Data Residency environments, and said GitHub Enterprise Server is not affected. They also said the rollout covers installation (server-to-server) tokens, including the Actions GITHUB_TOKEN, and that user-to-server tokens used in Copilot code review flows were not yet in scope. The October post says all newly minted installation tokens now use the new format by default. I did not find a newer statement on server or user-to-server tokens, so check GitHub’s current docs for your environment before treating either as settled.

The rule GitHub gives is the right one and the cheapest to adopt: treat the token as an opaque string. No length checks, no shape-based validation, no decoding. The one exception, which GitHub itself provides, is a recommended pattern for places that must recognize a token, such as redaction and scanning. Keep that pattern in one shared place so the next format change is a one-line edit.

Sources