📰 AI NewsAgileIntermediate✨ AI-assisted

GitHub App Installation Tokens Are Now 520 Characters Long

WittyTech Daily··4 min read
#github apps#tokens#secrets

Your GitHub App's installation token used to be 40 characters. It is now about 520, and any system that quietly assumed 40 is about to find out. GitHub has finished rolling out stateless GitHub App installation tokens, and the cleanup is on your side, not GitHub's.

What changed in GitHub App installation tokens

Per the GitHub changelog from October 2, 2026, a staged rollout that began on April 27, 2026 is now complete. All newly minted installation tokens use the stateless ghs_APPID_JWT format. GitHub says this makes token issuance and validation faster and improves the reliability of the GitHub API.

What stayed the same: the ghs_ prefix, the one-hour expiry, permissions, repository scoping and the installation access token endpoint. What changed is the length, from 40 characters to about 520. Tokens minted before the change keep working until they expire, so for a while you will see both formats side by side.

There is one deadline. The temporary X-GitHub-Stateless-S2S-Token header, a per-request override that let you test the new format on demand, is deprecated on November 30, 2026. After that GitHub stops respecting it, and eligible apps always receive stateless tokens. That is about six weeks away.

Why a longer token breaks things quietly

GitHub's own checklist names four places where the old length is likely baked in:

  • Validation that expects exactly 40 characters or the legacy pattern.
  • 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 redaction rules that only match the legacy pattern.

Truncation is the nasty one. A token cut short does not announce itself, it shows up as an authentication failure with no obvious cause. A missed redaction rule is worse: the token lands in your logs in plain text, and while it expires in an hour, your log retention probably does not.

What to do on Monday morning

The rule is simple: treat the token as an opaque string. No length checks, no format checks, no parsing the JWT inside it. Then work through these five checks.

1. Mint a token and look at its shape

Use the documented endpoint, and print only the prefix and length, never the token itself.

TOKEN=$(curl --silent --request POST \
  --url "https://api.github.com/app/installations/<installation-id>/access_tokens" \
  --header "Accept: application/vnd.github+json" \
  --header "Authorization: Bearer <app-jwt>" \
  --header "X-GitHub-Api-Version: 2026-03-10" | jq -r .token)

echo "prefix: ${TOKEN:0:4}"
echo "length: ${#TOKEN}"

You should see ghs_ and a length around 520. Replace <installation-id> with your installation ID and <app-jwt> with a JSON web token signed by your app.

2. Send it through your real network path

A token that works from your laptop proves little. Run this from the same host or pod your production code uses, behind the same proxy or gateway.

curl --silent --output /dev/null --write-out "%{http_code}\n" \
  --header "Accept: application/vnd.github+json" \
  --header "Authorization: Bearer $TOKEN" \
  --header "X-GitHub-Api-Version: 2026-03-10" \
  "https://api.github.com/installation/repositories"

Expect 200. A 401 here, when step 1 worked, points at something in between truncating the header.

3. Find columns that are too small

This query is for PostgreSQL. It lists short text columns whose names suggest they hold credentials.

SELECT table_name, column_name, character_maximum_length
FROM information_schema.columns
WHERE data_type IN ('character varying', 'character')
  AND character_maximum_length < 1024
  AND (column_name ILIKE '%token%'
    OR column_name ILIKE '%secret%'
    OR column_name ILIKE '%credential%');

GitHub says "about 520", which is not a maximum, so do not size a column to exactly 520. Switch to text with ALTER TABLE <table> ALTER COLUMN <column> TYPE text;. Also check any database in a non-strict mode (MySQL can be configured this way) that truncates oversized values instead of rejecting them.

Do the same audit for environment variable limits and secret store entries. Those limits vary by platform, so check your own.

4. Widen your redaction pattern

If your scrubber or secret scanner pins ghs_ tokens to 40 characters, it will miss the new ones. This pattern is my suggestion, not an official GitHub one, and it matches both formats.

import re

GHS_TOKEN = re.compile(r"ghs_[A-Za-z0-9_.-]+")

def redact(line: str) -> str:
    return GHS_TOKEN.sub("ghs_[REDACTED]", line)

Test it against a log line containing a token you just minted, then revoke or let that token expire.

5. Remove the deprecated header

Search every repository, including infrastructure and CI config, for the override.

git grep -n -i "X-GitHub-Stateless-S2S-Token"

Delete each hit before November 30, 2026. Test with the header gone and with a mix of old and new tokens, since pre-change tokens live until they expire.

A GitHub App token is a credential your agents and pipelines carry into production, so a format change you catch in a test run costs an afternoon, and one you catch from a truncated header during an incident costs far more.

← More in Agile