Skip to content

Add repository and worktree identities - #14921

Draft
williammartin wants to merge 2 commits into
williammartin-multiaccount-draft-01-accountfrom
williammartin-multiaccount-draft-02-repository
Draft

williammartin wants to merge 2 commits into
williammartin-multiaccount-draft-01-accountfrom
williammartin-multiaccount-draft-02-repository

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

Follow on from #14920

A one-command override still leaves account selection to repeat whenever a developer returns to a repository. This PR adds reusable identities and a repository binding shared by the main checkout and its linked worktrees.

Layer 2 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 created identities using both real accounts, inspected the generated Git files, and exercised repository-family selection, editing and cleanup. I verified the SSH aliases independently, cloned a private repository, and pushed with ordinary Git.

$ cat ~/.config/gh/identities/personal.gitconfig
# Managed by gh auth identity. Manual changes may be overwritten.
[gh-identity "github.com"]
	name = "personal"
	account = "williammartin"
	sshHost = "github-personal"
[credential "https://github.com"]
	username = "williammartin"

Create an identity and read its Git config file

multiaccount-layer-2-01a-Wiring---Layer-2---Identity-file-and-SSH-alias.mp4

A repository binding adds Git includes

multiaccount-layer-2-01b-Wiring---Layer-2---Repository-binding-files.mp4

Personal SSH identity: verified alias, private clone and ordinary Git push

multiaccount-layer-2-02-Personal-SSH-identity-verified-alias-private-clone-and-ordinary-Git-push.mp4

Delete and reset without signing out

multiaccount-layer-2-03-Delete-and-reset-without-signing-out.mp4

One identity for a repository family

multiaccount-layer-2-04-One-identity-for-a-repository-family.mp4

Reuse, edit, and unset an identity

multiaccount-layer-2-05-Reuse-edit-and-unset-an-identity.mp4

Key points

The identity file contains account metadata and Git preferences, not a credential. The global include loads auth.gitconfig, whose gitdir conditions select the identity for the common Git directory and linked worktrees.

An SSH alias is a declared connection route, not verified account ownership. The SSH recording checks the account separately with ssh -T. Unset, deletion and experiment reset retain saved accounts and credentials.

Notes for reviewers

This is the foundation layer. Directory and owner bindings, Git attribution, existing SSH-command compatibility and richer explanations are deliberately separate.

I am not sure an identity should support changing its account with gh auth identity edit --account. The account feels like part of the identity definition rather than an editable preference. The current implementation allows it, but that is an unresolved design question.

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/02-repository.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 a reusable identity references a saved account.

When I select it with gh auth use inside a repository.

Then the repository family uses that identity for API requests and independently invoked Git.

DO NOT MERGE

williammartin and others added 2 commits November 3, 2026 22:08
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
Copilot AI balanced review requested due to automatic review settings November 3, 2026 21:09

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 critical issues can change authentication in unbound repositories, use mismatched accounts for Git and API requests, and silently lose bindings during concurrent updates.

9 open issues
What changed in this PR

This draft prototype extends one-command account selection with reusable identities shared across a repository’s main checkout and linked worktrees. It connects those choices to API authentication and independently invoked Git; it is explicitly not intended to merge as-is.

Changes:

  • Adds identity management, repository selection, and experiment cleanup commands.
  • Introduces persistent bindings, conditional Git includes, HTTPS credential selection, and declared SSH routes.
  • Adds authentication, worktree, cleanup, and compatibility tests plus an isolated offline replay script.
File Description
script/​multiaccount-review/​02-repository.sh Adds an isolated repository-family replay.
pkg/​cmd/​root/​reset_experiment_test.go Tests experiment reset and confirmation.
pkg/​cmd/​root/​account.go Updates account-selection error guidance.
pkg/​cmd/​root/​account_git_test.go Extends override tests to bound identities and SSH.
pkg/​cmd/​auth/​use/​worktrees_test.go Tests shared worktree selection and conflicts.
pkg/​cmd/​auth/​use/​use.go Adds repository binding and inspection commands.
pkg/​cmd/​auth/​use/​use_test.go Tests selection validation and setup rollback.
pkg/​cmd/​auth/​identity/​ssh.go Executes the selected SSH route.
pkg/​cmd/​auth/​identity/​identity.go Adds identity management and dispatcher commands.
pkg/​cmd/​auth/​identity/​identity_test.go Adds isolated command-test helpers.
pkg/​cmd/​auth/​identity/​delete.go Adds confirmed deletion and experiment reset.
pkg/​cmd/​auth/​identity/​delete_test.go Tests cleanup, cancellation, and rollback.
pkg/​cmd/​auth/​gitcredential/​identity_test.go Tests bound-account HTTPS credentials.
pkg/​cmd/​auth/​gitcredential/​gist_production_test.go Tests gist selection with production configuration.
pkg/​cmd/​auth/​gitcredential/​gist_identity_test.go Tests canonical-host identity selection for gists.
pkg/​cmd/​auth/​auth.go Registers identity, selection, and reset commands.
internal/​identity/​worktree_transport_test.go Tests API authentication from linked worktrees.
internal/​identity/​store.go Implements identity storage, bindings, and Git integration.
internal/​identity/​store_test.go Tests binding behavior and live identity edits.
internal/​identity/​ssh.go Resolves persistent and invocation-specific SSH routes.
internal/​identity/​ssh_test.go Tests SSH routing and user preservation.
internal/​identity/​ssh_command.go Validates underlying SSH configuration.
internal/​identity/​hardening_test.go Tests unsupported layouts and helper conflicts.
internal/​identity/​delete.go Implements staged deletion and integration cleanup.
internal/​identity/​credentials.go Resolves bound credentials and environment conflicts.
internal/​identity/​credentials_test.go Tests identity-aware authentication and token conflicts.
internal/​identity/​account_test.go Tests account-specific SSH route selection.
internal/​identity/​account_status.go Reports missing saved-account registrations.
internal/​gh/​gh.go Adds effective credential resolution to AuthConfig.
internal/​config/​config.go Applies repository identities to API and Git credentials.
git/​client.go Preserves SSH environment settings for override validation.
api/​identity_test.go Tests bound identities and canonical gateway credentials.
api/​identity_gateway_test.go Tests gateway credential precedence.

Review effort: Balanced

Comment thread internal/config/config.go
Comment on lines +324 to +325
if selected != nil {
return identity.CredentialFor(c, selected)
Comment thread internal/identity/ssh.go
if account != "" {
id, err = s.accountSSHRoute(host, owner, dir, overrideHost, account)
} else {
id, err = s.Resolve(host, owner, dir)
Comment on lines +354 to +356
if err := s.supportedCredentialHelpers(contextDir, b.Host); err != nil {
return err
}
}
}
}
bindings, err := s.Bindings()
Comment on lines +579 to +580
fmt.Fprintf(&config, "[credential %s]\n\thelper =\n\thelper = %s\n\tuseHttpPath = true\n",
gitQuote("https://"+host), gitQuote(helper))
return gh.Credential{}, errors.New(warning)
}
credential, status, err := auth.TokenForUserWithRefresh(id.Host, id.Account)
if err != nil {
Comment on lines +489 to +493
normalized, normErr := normalizeBinding(b)
if normErr != nil {
return normErr
}
if normalized.Repository != "" {
Comment on lines +577 to +580
for _, host := range sortedKeys(hosts) {
helper := "!" + shellquote.Join(executable, "auth", "git-credential")
fmt.Fprintf(&config, "[credential %s]\n\thelper =\n\thelper = %s\n\tuseHttpPath = true\n",
gitQuote("https://"+host), gitQuote(helper))
Comment on lines +3 to +15
import (
"os"
"path/filepath"
"testing"

"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/identity"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestResetExperimentDetachesIntegrationWithoutDeletingAccounts(t *testing.T) {

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.

2 participants