[Bug]: GitHub credentials ignore the project checkout since the API transport, so per-checkout gh accounts on one host can't read their PRs#17004

opened20d agobyEevbuildsnet

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

Two accounts on github.com: account-a can see private repos in example-org-a, account-b can see private repos in example-org-b. Neither can see the other org. Both are logged in with gh auth login.

  1. Route gh per checkout with a wrapper earlier on PATH than the real binary, keyed off a Git config value:

    sh
    1#!/bin/sh
    2# ~/bin/gh
    3REAL_GH=/opt/homebrew/bin/gh
    4account=$(git config --get gh.user 2>/dev/null || echo account-a)
    5GH_TOKEN=$("$REAL_GH" auth token --hostname github.com --user "$account") || exit 1
    6export GH_TOKEN
    7exec "$REAL_GH" "$@"
    ini
    1# ~/.gitconfig
    2[includeIf "gitdir:~/work/org-b/"]
    3 path = ~/work/org-b/.gitconfig
    4
    5# ~/work/org-b/.gitconfig
    6[gh]
    7 user = account-b
  2. Check it in a terminal:

    sh
    1cd ~/work/org-a/gadget && gh api user --jq .login # account-a
    2cd ~/work/org-b/widget && gh api user --jq .login # account-b
    3gh pr view 123 --json state # works
  3. Add ~/work/org-a/gadget and ~/work/org-b/widget as T3 Code projects. Link example-org-b/widget#123 to a thread in the widget project.

  4. Watch the PR badge, sidebar status, and PR pane for that thread.

Expected behavior

Each project's GitHub reads use the account its own checkout resolves to, as they did before the API transport landed. Back then, PR reads ran gh in the project checkout. example-org-b/widget#123 shows its real status and detail.

Actual behavior

PRs in example-org-b never resolve. Status stays unknown or null, and detail reads fail as not found, because GitHub answers a private repo the token can't see that way. git fetch and gh in the same checkout keep working. PRs in example-org-a are fine.

Cause, at 611132c171:

  • GitHubCredentials runs gh auth token --hostname <host> [--user <Settings account>] with cwd: HostProcessWorkingDirectory, the server's own directory (my home directory), not the project checkout. (GitHubCredentials.ts#L139-L161)
  • The cache key is host plus the optional Settings account, with no project or cwd. A token is cached for 5 minutes and a missing one for 10 seconds. (GitHubCredentials.ts#L198-L200)
  • GitHubApi.credential(host) takes only the host. The cwd that GitHubPullRequestCli requests still carry is dropped before the credential lookup.

Precedence is a saved token, then GH_TOKEN, then gh. So every project on github.com gets one credential, whichever account gh returns from the server's directory.

None of these get both orgs working at the same time:

  • Settings → Source Control → GitHub account picker (#16322): picking account-b fixes example-org-b and breaks example-org-a. The choice is per host, not per project.
  • gh auth switch: same trade-off, and this wrapper names its fallback account explicitly anyway.
  • Git per-path credentials (credential.useHttpPath): don't help, because T3 only asks gh auth token --hostname.

Impact

Major degradation or frequent failure

Version or commit

0.0.46-nightly.20261007.2774 (611132c171). Regressed with #16319 / #16320 / #16322.

Environment

macOS, desktop app, gh 2.x, two github.com accounts routed per checkout

Logs or stack traces

shell
1# Sanitized. Same checkout, terminal vs T3:
2$ cd ~/work/org-b/widget && gh pr view 123 --json state --jq .state
3OPEN
4# T3: the PR status for example-org-b/widget#123 stays unknown and the API read fails as not found.
5# The credential came from the server's directory, so it belongs to account-a.

Workaround

None that serves both orgs at once. Switching the per-host account in Settings serves one org at a time.

Possible fix: with no saved or env token, run gh auth token from the request's cwd, and key the credential cache by cwd or by the resulting fingerprint. Batched reads would also need grouping by resolved credential, which is what #14218 does. A local patch that threads cwd through GitHubApi / GitHubCredentials and makes the caches and batches cwd-aware brought back correct per-checkout accounts and PR status in my testing.

Related: #14123 (same per-directory setup, from before the API transport), #16068 (Authored viewer per host), and the 2026-10-07 discussion on #14218 about restoring per-workspace gh auth token.

Synced from GitHub

The per-path Git credentials limitation noted here also affects multiple organisation-scoped PATs for a single GitHub login, including repository lookup before a checkout exists. I recorded that narrower reproduction in #21264 and submitted #21265.

The PR adds an opt-in route through the server's existing global Git HTTPS helpers. It preserves original remote spelling for case-sensitive helper matching, scopes captured credentials to the requested repository, and keeps caches/batches separated by credential even for the same viewer. Helper failures stop safely, and an API denial never retries with a different token. Checkout-local helpers and per-directory gh wrappers are unchanged. Verification includes real case-sensitive Git stores and native remote lookup, clone, New Project completion and mixed PR reads.

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Thanks for the report. Issue #16068 already tracks this problem, so we close this issue to keep all detail in one place. Please follow #16068 and add your version and steps there if they differ. Please do not open a new issue for the same problem.

Duplicate of #14123: both report linked PR status and detail failing for the second private organization when GitHub accounts are routed by checkout. This report adds the API-transport credential lookup and cache details. #16068 covers the separate Authored-filter symptom. Compared the issue reports and comments; no runtime reproduction was performed.

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Correction to our earlier close: this duplicates #14123, not #16068. Please follow #14123 for this problem.