Document the GCM credential failure, and add a README #94

Merged
liamjd merged 8 commits from claude-md-git-credential-note into main 2026-09-13 17:37:33 +01:00
Owner

Documents the git push credential failure hit during #80, so the next person to see it — the laptop, or either of us in three months — does not re-derive it.

Why this is worth writing down

The symptom is maximally misleading. fj issue create succeeded moments before the push failed, so it looked like a forgejo problem and was not. And the push dies on Credentials are incorrect or have expired with no prompt to answer, which reads like a server fault rather than a client one.

The actual cause

Git Credential Manager's generic provider sniffs the WWW-Authenticate: Basic realm="Gitea" header, recognises forgejo, and applies a built-in OAuth client. Per GCM's own docs that detection is automatic and not configurable — no git config key, no environment variable. So the credential.https://git.liamjd.net.provider=generic line in ~/.gitconfig never meant "use basic auth", which is exactly what sent the diagnosis down the wrong path twice.

The silent failure itself is GCM issue #1408 — on expiry GCM erases the stored credential but never starts a renewal flow. Open since 2.3.2, still present in the 2.5.0 installed here.

The fix, verified

Override the credential helper to wincred for this host only, so GCM stays the default everywhere else:

git config --global --add credential.https://git.liamjd.net.helper ""
git config --global --add credential.https://git.liamjd.net.helper wincred

Confirmed working end to end: git prompts in the terminal, and forgejo accepts an access token as the password. A token with no expiry set cannot hit the renewal bug.

Getting there took three inconclusive push tests, and the doc now warns about why. Changing the config proves nothing while a credential is still cached — the store gets reused and the push succeeds without ever exercising the new path. Worse, the two helpers use different target names (GCM writes the bare host, wincred writes <user>@<host>), so clearing "the" credential can leave one behind and look like a fix that did not take.

Also recorded

  • SSH is ruled out, with the evidence. It is the obvious fix and it does not work: git.liamjd.net resolves to Cloudflare with only 443 open, and the host answers on no LAN address, so the forgejo container's 222:22 mapping is never reached. Cloudflare's proxy carries HTTP/HTTPS only. Written down because reaching for SSH first cost a wasted key-generation round trip.
  • fj pr create needs --head and --base explicitly, where gh would infer them from the checkout.
  • git commit --allow-empty as the way to test a push without committing a stray file.

Note on the commits

Four commits rather than one, and deliberately not squashed: the first records the wrong diagnosis and the second corrects it. Flattening would hide that provider=generic looks like the answer and is not, which is the trap worth leaving visible in the history.

A README.md also rides along. It began as a throwaway push test — the exact mistake the doc now warns about — but the repo had no README, so rather than revert it, it is now a real one: what the game is, what works today versus what has not been started, how to run and test it, the design constraints that bind the data model rather than the content, and an index of docs/. Every link target was checked against the working tree.

It also records the two engine traps up front, because they cost time and are not discoverable from the code: adding a class_name needs an editor scan rather than an import pass, and headless renders nothing under GL Compatibility, so a green test suite says nothing about how anything looks.

🤖 Generated with Claude Code

https://claude.ai/code/session_012V5CmAgcjna6fBrgZebqjZ

Documents the git push credential failure hit during #80, so the next person to see it — the laptop, or either of us in three months — does not re-derive it. ## Why this is worth writing down The symptom is maximally misleading. `fj issue create` succeeded moments before the push failed, so it looked like a forgejo problem and was not. And the push dies on `Credentials are incorrect or have expired` **with no prompt to answer**, which reads like a server fault rather than a client one. ## The actual cause Git Credential Manager's generic provider sniffs the `WWW-Authenticate: Basic realm="Gitea"` header, recognises forgejo, and applies a built-in OAuth client. Per GCM's own docs that detection is automatic and **not configurable** — no git config key, no environment variable. So the `credential.https://git.liamjd.net.provider=generic` line in `~/.gitconfig` never meant "use basic auth", which is exactly what sent the diagnosis down the wrong path twice. The silent failure itself is [GCM issue #1408](https://github.com/git-ecosystem/git-credential-manager/issues/1408) — on expiry GCM erases the stored credential but never starts a renewal flow. Open since 2.3.2, still present in the 2.5.0 installed here. ## The fix, verified Override the credential helper to `wincred` for this host only, so GCM stays the default everywhere else: ```sh git config --global --add credential.https://git.liamjd.net.helper "" git config --global --add credential.https://git.liamjd.net.helper wincred ``` Confirmed working end to end: git prompts in the terminal, and forgejo accepts an access token as the password. A token with no expiry set cannot hit the renewal bug. Getting there took three inconclusive push tests, and the doc now warns about why. **Changing the config proves nothing while a credential is still cached** — the store gets reused and the push succeeds without ever exercising the new path. Worse, the two helpers use different target names (GCM writes the bare host, `wincred` writes `<user>@<host>`), so clearing "the" credential can leave one behind and look like a fix that did not take. ## Also recorded - **SSH is ruled out, with the evidence.** It is the obvious fix and it does not work: `git.liamjd.net` resolves to Cloudflare with only 443 open, and the host answers on no LAN address, so the forgejo container's `222:22` mapping is never reached. Cloudflare's proxy carries HTTP/HTTPS only. Written down because reaching for SSH first cost a wasted key-generation round trip. - `fj pr create` needs `--head` and `--base` explicitly, where `gh` would infer them from the checkout. - `git commit --allow-empty` as the way to test a push without committing a stray file. ## Note on the commits Four commits rather than one, and deliberately not squashed: the first records the wrong diagnosis and the second corrects it. Flattening would hide that `provider=generic` looks like the answer and is not, which is the trap worth leaving visible in the history. A `README.md` also rides along. It began as a throwaway push test — the exact mistake the doc now warns about — but the repo had no README, so rather than revert it, it is now a real one: what the game is, what works today versus what has not been started, how to run and test it, the design constraints that bind the data model rather than the content, and an index of `docs/`. Every link target was checked against the working tree. It also records the two engine traps up front, because they cost time and are not discoverable from the code: adding a `class_name` needs an editor scan rather than an import pass, and headless renders nothing under GL Compatibility, so a green test suite says nothing about how anything looks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012V5CmAgcjna6fBrgZebqjZ
The push failure during #80 looked like an fj problem, because fj had
just created an issue successfully moments earlier. It was not: GCM was
serving a stale OAuth token for git.liamjd.net and never prompting, with
the provider=generic setting inert because it was added after the OAuth
credential was already cached.

Records the symptom, how to recognise it in cmdkey /list, and why both
the token and its refresh_token companion have to be deleted. Also notes
that SSH sidesteps the mechanism entirely, and that fj pr create needs
--head and --base explicitly where gh would infer them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012V5CmAgcjna6fBrgZebqjZ
The push failure during #80 looked like an fj problem, because fj had
just created an issue successfully moments earlier. It was not: GCM was
serving a stale OAuth token for git.liamjd.net and never prompting, with
the provider=generic setting inert because it was added after the OAuth
credential was already cached.

Records the symptom, how to recognise it in cmdkey /list, and why both
the token and its refresh_token companion have to be deleted. Also notes
that fj pr create needs --head and --base explicitly where gh would
infer them.

Rules SSH out explicitly, with the evidence, because it is the obvious
fix and it does not work: the host is behind a Cloudflare Tunnel with
only 443 open, so the container's 222:22 mapping is never reached and
ssh -T times out rather than failing usefully. HTTPS with an access
token is the supported path here, not a workaround.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012V5CmAgcjna6fBrgZebqjZ
The previous commit blamed config ordering - provider=generic supposedly
inert because it was added after an OAuth credential was cached. Clearing
the credential disproved that: GCM re-prompted and went straight back to
OAuth.

The real cause is upstream. GCM's generic provider sniffs the
WWW-Authenticate "Basic realm=Gitea" header, recognises forgejo and
applies a built-in OAuth client, and that detection is automatic with no
config key or environment variable to disable it. provider=generic never
meant basic auth. The silent failure on expiry is GCM issue #1408, where
the stale credential is erased but no renewal flow starts - open since
2.3.2 and still present in 2.5.0.

Records the wincred per-host helper override as the way off OAuth
entirely, since a forgejo access token with no expiry cannot hit the
renewal bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012V5CmAgcjna6fBrgZebqjZ
The override is confirmed working: with the helper chain reset and the
store cleared, git prompts in the terminal and forgejo accepts an access
token as the password.

Getting there took three inconclusive push tests, all for the same
reason: changing the config does nothing while a credential is still
cached, because whatever is in the store gets reused and the push
succeeds without exercising the new path. Worse, the two helpers use
different target names - GCM writes the bare host, wincred writes
user@host - so clearing "the" credential can leave one behind.

Notes both names, and --allow-empty as the way to test a push without
committing a stray file to the branch you happen to be on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012V5CmAgcjna6fBrgZebqjZ
Replaces the two-line stub that arrived as a push test. Aimed at someone
landing on the repo cold: what the game is, what actually works today
versus what has not been started, how to run it and test it, and where
the design docs are.

Includes the design constraints that have teeth - deterministic threat
cycles, no upkeep denominated in a building's own output, the two
difficulty axes - because they constrain the data model rather than the
content, and a newcomer reading only the code would not find them.

Also records the two engine traps up front: a class_name needs an editor
scan rather than an import pass, and headless renders nothing under GL
Compatibility, so a green suite says nothing about how anything looks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012V5CmAgcjna6fBrgZebqjZ
liamjd changed title from Document the GCM credential failure against forgejo to Document the GCM credential failure, and add a README 2026-09-13 15:32:25 +01:00
liamjd merged commit b08e5de113 into main 2026-09-13 17:37:33 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
liamjd/UntitledColonyBuilder!94
No description provided.