Repository navigation
feat: pick which GitHub account a project talks to, and approve pull requests from lich - #107
Merged
Merged
Conversation
gh keeps one active account per host, globally. A repository only a second account can see answers every lookup with "Could not resolve to a Repository" — the same thing GitHub says for a repository that does not exist — so the pull request screen read as broken on any project the active account cannot see. A project now names the account its GitHub calls run as: vcs.account in the store, resolved to a token through `gh auth token --user` and handed to gh in GH_TOKEN. Every gh call goes through one seam (Service.gh), so the badge, the list, the detail, the checks, the diff, the merge and the PR checkout all answer as the same account. Left unset, nothing changes. The picker is a new Version Control section in Settings, filled from `gh auth status`. The badge was the one caller that shelled out to gh on its own; it now takes the same path, which is what made it obey the project's account. The account governs what lich reads from GitHub, not what git does — a push still rides the remote's ssh key and the global user.email. That gap is a Known Ceiling now. While the screen was the subject: neither tool's stderr reaches it anymore. gh's GraphQL envelopes and git's "fatal:" lines (and, when git refuses by exit status alone, the words "exit status 1") are replaced by a sentence per failure lich can hit — the invisible repository points at the account picker, a signed-out gh names `gh auth login`, a dirty worktree says where to remove it. The raw text goes to lich's log, which is where it was always the more useful half.
Open in Session failed on exactly the repositories the account picker exists for: "GitHub does not show this repository to the account lich is using here." `gh pr checkout` runs inside the worktree it just created, and the account is resolved from the directory gh runs in. That worktree has no session row yet — the frontend opens one only after this call returns — so the store had no project to map it to, the token came back empty, and the checkout fell back to gh's active account. The account is a property of the project, not of the directory a command happens to run in, so the two are now separate arguments (ghFor): the checkout runs in the worktree and answers to the project.
Approving was the one step of a review that still meant opening the browser. An Approve button now sits beside Merge and files the approving review through gh, as the account the project talks to. Once the review lands the button reads Approved and stops offering itself: GitHub would take a second one, but nobody means to send it. The gate is not the merge gate — a draft and a conflicting pull request can both still be reviewed, only one that is already merged or closed cannot. GitHub refuses an account approving its own pull request, so that refusal joins the table and reads as a sentence. Reviews with a body and requesting changes are deliberately absent: a review comment belongs to the line it is about, and there is nowhere to attach one yet.
This was referenced Jul 27, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
ghkeeps one active account per host, globally. On a machine signed in as two GitHub accounts, a private repository only the second one can see answers every lookup with:GitHub does not distinguish 404 from 403, so the message reads as "this repository does not exist" — and lich pasted it on screen verbatim, command prefix and all. The screen looked broken on a repository that was fine.
What this does
A project names the GitHub account its calls run as.
vcs.accountin the store (project-scoped — gh already has a global answer, and this exists to override it for one repository), resolved to a token viagh auth token --user <login>and passed to gh inGH_TOKEN. Every gh call routes through one seam (Service.gh), so the badge, the list, the detail, the checks, the diff, the merge and the PR checkout all answer as the same account. Unset, nothing changes.A checkout is addressed by path everywhere in the project services, so the path → project mapping is resolved in the store: the project's own directory, or a session's (a worktree lives outside the project directory, so only its session row ties it back). No RPC signature changed.
A new Version Control section in Settings picks the account, filled from
gh auth status.The badge was shelling out to gh on its own, outside the seam — it would have kept answering as gh's active account while the screen beside it used the project's, vanishing on exactly the repositories the picker exists for. It takes the same path now.
Neither tool's stderr reaches the screen anymore.
gherror.goandgiterror.goeach hold a table of the failures lich can actually hit;toolerror.goholds what they share. gh's GraphQL envelopes, git'sfatal:lines, and — when git refuses by exit status alone (check-ref-format) — the literal wordsexit status 1are replaced by a sentence that says what to do. The raw text goes to lich's log, which is where it was always the more useful half.GraphQL: Could not resolve to a Repository with the name '…'To get started with GitHub CLI, please run: gh auth logingh auth loginin a terminal.git check-ref-format: exit status 1"has space"is not a name git accepts for a branch.fatal: '…' contains modified or untracked files, use --force to delete itApproving a pull request no longer means opening the browser. An Approve button sits beside Merge and files the approving review through the project's account. Once it lands the button reads Approved and stops offering itself — GitHub would take a second review, but nobody means to send one. The gate is not the merge gate: a draft and a conflicting pull request can both still be reviewed, only one already merged or closed cannot. Reviews with a body and requesting changes are deliberately absent — a review comment belongs to the line it is about, and lich has nowhere to attach one yet.
Ceiling
The account governs what lich reads from GitHub, not what git does: a push still rides the remote's ssh key and signs with the global
user.email, so a PR can be read by one account and its commits land under another with no error anywhere. Documented as a Known Ceiling; per-worktreecore.sshCommand+user.emailis the upgrade path.Test plan
Could not resolve to a Repository; after setting the account — the list of open PRs.project.GitHubAccountsagainst the realgh auth statusreturns["omartelo", "marcelo-filho_snk"].gh's active account+ both logins, the stored value is selected, no page errors.TestGHMessageLeaksNothing/TestGitMessageLeaksNothingassert the rule, not just the cases: no message may containgraphql,http 4xx,exit status,fatal:,hint:,--force.TestRunGitMessageruns real git against a non-repository and asserts the sentence — the whole path, not just the table.go test ./...,gofmt -l .,go vet ./...clean; frontendbiome check(0 errors),vitest468 passed,tsc --noEmitclean,vite buildok.gh pr checkoutruns inside the worktree it just created, which has no session row yet, so the account resolved to none and the checkout fell back to gh's active one. Fixed by separating "where the command runs" from "whose account it runs as" (ghFor); the regression test fails without the fix withtoken "".Review Can not approve your own pull request.Five backend tests changed: they asserted the command prefix and the tool's own words, which is precisely what leaked. Each now asserts the message passes through unchanged.
🤖 Generated with Claude Code