Skip to content

Add one-command account selection - #14920

Draft
williammartin wants to merge 3 commits into
trunkfrom
williammartin-multiaccount-draft-01-account
Draft

williammartin wants to merge 3 commits into
trunkfrom
williammartin-multiaccount-draft-01-account

Conversation

@williammartin

@williammartin williammartin commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Prototype overview and documentation

DO NOT MERGE. This stack is almost entirely vibe coded. These drafts are for handy review and prototyping, not merging as-is.

Expect the code to be really bad in places: highly coupled, handling edge cases that do not really matter, or doing suspiciously fragile things like parsing GraphQL request bodies.

I suspect some of the suspicious code also exposes the complexity of this feature set, because account selection, Git configuration, scope precedence, worktrees, SSH and API requests all interact.

The prototype experience is the purpose, not the code. I want to find out whether the experience is useful, not defend this implementation or treat it as a design to preserve.

Description

Switching the saved active account changes later commands too, which makes parallel work with different accounts awkward. This PR adds --account LOGIN for one command without changing the saved default.

Layer 1 of an eight-part prototype review stack. This is a way to review the design and implementation in smaller pieces, not a proposal to merge the current stack as-is.

How did you test this change?

I used both saved accounts for API requests and saw the default remain unchanged after the override. I also cloned a private repository over HTTPS and saw SSH account routing refused at this layer.

$ gh api /user --account wilmartin_microsoft --jq .login
wilmartin_microsoft

One-command account selection: real SSH refusal and private HTTPS clone

multiaccount-layer-1-02-One-command-account-selection-real-SSH-refusal-and-private-HTTPS-clone.mp4

One command, another real account

multiaccount-layer-1-03-One-command-another-real-account.mp4

Refuse an unsaved account

multiaccount-layer-1-04-Refuse-an-unsaved-account.mp4

Key points

The authentication configuration resolves the effective account. Child Git processes receive invocation-local selection and credential-helper settings, not a persisted account switch. Plain Git commands do not acquire an authentication dependency.

SSH routing is deliberately unsupported here. The next layer adds identities with declared SSH aliases.

Notes for reviewers

Start with the distinction between the effective account and the saved default, then follow selection into API requests and authenticated Git subprocesses. Explicit request headers and cross-host redirect rules still belong to the shared API authentication transport.

The recordings use the product code at this layer, not a later stack endpoint. The included offline replay scripts are supplementary and do not reproduce the live-account recordings.

Offline replay on macOS

Build this layer, then run its synthetic scenario with an absolute executable path. This is a replay instruction, not a claim that the command below was used to make the videos.

make
env -i HOME="$HOME" PATH=/usr/bin:/bin:/opt/homebrew/bin \
  /bin/bash script/multiaccount-review/01-account.sh --gh "$PWD/bin/gh"

Authorship and follow-up

Who wrote this:

  • A human wrote it.
  • An agent wrote it under close human direction.
  • An agent wrote it independently, and no human has guided the implementation beyond the initial prompt.

Who answers review comments:

  • @username will read and reply directly. Name the account.
  • An agent will draft replies and @username will read them before they are posted.
  • Nobody has explicitly committed to replying.

Acceptance Criteria

Prototype behaviour under review:

Given both accounts are saved for the host.

When I pass --account wilmartin_microsoft to a supported command.

Then that invocation uses the requested account without changing the saved default.

DO NOT MERGE

williammartin and others added 3 commits November 2, 2026 16:07
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 965836ef-19ac-4473-ba1f-e326ce0f106b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 965836ef-19ac-4473-ba1f-e326ce0f106b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 3, 2026 21:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved authentication regressions can crash existing git clients, replace exact-host credentials with another host’s credentials, and apply account overrides to hosts the commands do not target.

7 open issues
What changed in this PR

This draft prototype adds --account LOGIN to select a saved account for one gh invocation without changing the saved default. It connects account selection to API requests and authenticated Git subprocesses; SSH account routing is deliberately unsupported at this layer.

Changes:

  • Add command-level account selection and distinguish effective authentication from the persisted default.
  • Forward selection to Git credential helpers and introduce resolver-based HTTP authentication.
  • Add regression tests and an isolated macOS offline replay scenario.
File Description
script/​multiaccount-review/​README.md Document replay usage, isolation, and limitations.
script/​multiaccount-review/​01-account.sh Add an offline account-selection replay.
pkg/​cmd/​skills/​publish/​publish_test.go Supply authentication callbacks in publish fixtures.
pkg/​cmd/​root/​root.go Register the account flag and selection hooks.
pkg/​cmd/​root/​help.go Hide the inherited account flag in auth help.
pkg/​cmd/​root/​account.go Validate account selection and determine its host.
pkg/​cmd/​root/​account_test.go Test invocation-local selection and host handling.
pkg/​cmd/​root/​account_git_test.go Test HTTPS selection and SSH refusal.
pkg/​cmd/​repo/​fork/​fork_test.go Update Git authentication fixtures.
pkg/​cmd/​repo/​create/​create_test.go Update Git authentication fixtures.
pkg/​cmd/​repo/​clone/​clone_test.go Update Git authentication fixtures.
pkg/​cmd/​pr/​merge/​merge_test.go Update Git authentication fixtures.
pkg/​cmd/​pr/​create/​create_test.go Update Git authentication fixtures.
pkg/​cmd/​pr/​checkout/​checkout_test.go Update Git authentication fixtures.
pkg/​cmd/​issue/​develop/​develop_test.go Update Git authentication fixtures.
pkg/​cmd/​gist/​clone/​clone_test.go Update Git authentication fixtures.
pkg/​cmd/​factory/​git_authentication_test.go Test deferred selection and unchanged ordinary SSH behavior.
pkg/​cmd/​factory/​default.go Wire API resolvers and Git authentication context.
pkg/​cmd/​extension/​mocks.go Mock authenticated remote-version lookup.
pkg/​cmd/​extension/​git.go Add authenticated ls-remote support.
pkg/​cmd/​extension/​git_test.go Test authentication failure propagation.
pkg/​cmd/​extension/​extension.go Use authenticated extension-version lookup.
pkg/​cmd/​auth/​token/​token.go Resolve the persisted default username.
pkg/​cmd/​auth/​switch/​switch.go Use persisted account state when switching.
pkg/​cmd/​auth/​status/​status.go Report the configured default account.
pkg/​cmd/​auth/​refresh/​refresh.go Use the configured username during refresh.
pkg/​cmd/​auth/​logout/​logout.go Compare persisted defaults around logout.
pkg/​cmd/​auth/​gitcredential/​helper.go Delegate credential selection to authentication config.
pkg/​cmd/​auth/​gitcredential/​helper_test.go Adapt the credential-resolution test double.
pkg/​cmd/​auth/​gitcredential/​gist_production_test.go Test gist selection with production configuration.
pkg/​cmd/​auth/​gitcredential/​gist_identity_test.go Add credential-test isolation helpers.
pkg/​cmd/​auth/​auth.go Add a hidden helper that refuses SSH routing.
pkg/​cmd/​api/​api.go Use effective-account token resolution.
pkg/​cmd/​api/​api_test.go Update authentication configuration fixtures.
internal/​identity/​credentials.go Add account-specific and API token resolvers.
internal/​identity/​credentials_test.go Test gateway selection and request authentication boundaries.
internal/​gh/​gh.go Extend the authentication configuration contract.
internal/​config/​test.go Clear inherited account-selection variables in tests.
internal/​config/​refreshable_test.go Check persisted account activation state.
internal/​config/​config.go Implement effective account and Git credential selection.
internal/​config/​auth_config_test.go Update unknown-user expectations.
internal/​config/​active_account_test.go Test selection precedence and default preservation.
git/​client.go Apply invocation-local authentication to Git subprocesses.
git/​client_test.go Supply authentication callbacks in existing tests.
git/​authenticated_command_test.go Test deferred authentication and local-command independence.
api/​http_client.go Add error-aware token resolvers and clone authenticated requests.
api/​auth_resolver_test.go Test resolver failures and conflicting token sources.

Review effort: Balanced

Comment thread git/client.go
Comment on lines +174 to +177
auth, err := c.Authentication()
if err != nil {
return nil, err
}
Comment on lines +23 to +25
if canonical, ok := auth.HostForAPIHost(host); ok {
host = canonical
}
Comment thread pkg/cmd/root/account.go
// such as env var, command-line flags, repository remotes, and more. I'd honestly be a bit shocked if this logic is correct in all cases.
repositoryArgument := strings.Contains(cmd.Use, "<repository>") ||
parent == "repo" && (cmd.Name() == "create" || cmd.Name() == "sync")
resourceURL := cmd.Name() == "api" || parent == "pr" || parent == "issue" || parent == "gist"
Comment thread pkg/cmd/root/account.go
Comment on lines +117 to +119
if cmd.Name() == "create" || cmd.Name() == "clone" {
return host, nil
}
Comment thread pkg/cmd/root/account.go
return host, nil
}
}
repositoryContext := parent == "repo" && strings.Contains(cmd.Use, "repository")
Comment thread pkg/cmd/root/account.go
Comment on lines +150 to +152
remotes, err := client.Remotes(context.Background())
if err == nil && len(remotes) > 0 && remotes[0].FetchURL != nil {
return remotes[0].FetchURL.Host, nil
Comment thread pkg/cmd/root/account.go
if repositoryContext {
remotes, err := f.Remotes()
if err == nil && len(remotes) > 0 {
return remotes[0].RepoHost(), nil
@slowmo3two-design

This comment was marked as spam.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants