Skip to content
vast-cow's blog
Go back

Using Multiple GitHub Accounts: Automatically Select the Correct `gh` PAT Based on Repo Owner

Edit page

When using multiple GitHub accounts, the process of selecting the correct Personal Access Token (PAT) for git push or git pull over HTTPS can be a bit cumbersome.

Specifically, when dealing with repositories from multiple owners, such as:

https://github.com/aont/foo.git
https://github.com/another-user/bar.git

Git Credential Manager or gh auth git-credential might default to the currently active account, leading to errors like:

remote: Permission to owner/repo.git denied to other-user.

To address this, the owner portion of the remote URL can be used to determine the GitHub CLI account, and a credential helper can be embedded directly into the .gitconfig file. This helper retrieves the PAT using:

gh auth token --user ACCOUNT

and provides it to Git.

For personal repositories, the repository owner can be used directly as the account name. For organization-owned repositories, where the repository owner and the account used for authentication differ, explicit mappings can be added.

This eliminates the need for external scripts.

Prerequisites

First, ensure that you are logged into your GitHub accounts using gh:

gh auth login

If you have multiple accounts registered, you can verify them with:

gh auth status

With the basic method, if a repository URL is:

https://github.com/aont/foo.git

the command:

gh auth token --user aont

will be executed.

Therefore, the default assumption is:

repository owner = gh registered account name

When this assumption does not hold, explicit mappings can override it.

.gitconfig

A configuration supporting both the default owner-based behavior and explicit account mappings can be written as follows:

[credential]
    useHttpPath = true

[credential "https://github.com/"]
    helper =
    helper = "!f() { \
        [ \"$1\" = get ] || exit 0; \
        path=''; \
        while IFS='=' read -r k v; do \
            [ \"$k\" = path ] && path=$v; \
        done; \
        [ -n \"$path\" ] || exit 0; \
        owner=${path%%/*}; \
        repo=${path#*/}; \
        repo=${repo%.git}; \
        case \"$owner/$repo\" in \
            my-org/special-repo) account='special-account' ;; \
            *) \
                case \"$owner\" in \
                    my-org) account='my-work-account' ;; \
                    another-org) account='another-account' ;; \
                    *) account=\"$owner\" ;; \
                esac \
                ;; \
        esac; \
        token=$(gh auth token --user \"$account\") || exit 0; \
        printf 'username=%s\\npassword=%s\\n' \"$account\" \"$token\"; \
    }; f"

[credential "https://gist.github.com/"]
    helper =
    helper = "!f() { \
        [ \"$1\" = get ] || exit 0; \
        account=''; \
        while IFS='=' read -r k v; do \
            [ \"$k\" = path ] && account=${v%%/*}; \
        done; \
        [ -n \"$account\" ] || exit 0; \
        token=$(gh auth token --user \"$account\") || exit 0; \
        printf 'username=%s\\npassword=%s\\n' \"$account\" \"$token\"; \
    }; f"

There are four key points.

credential.useHttpPath = true is Important

Typically, Git’s credential helper receives only:

protocol=https
host=github.com

However, in this case, we need the path:

aont/foo.git

Therefore, set:

[credential]
    useHttpPath = true

This causes the path to be passed to the credential helper’s standard input, such as:

protocol=https
host=github.com
path=aont/foo.git

Use the Beginning of the Path as the Default Account

Inside the helper, the path is first stored:

path=''
while IFS='=' read -r k v; do
    [ "$k" = path ] && path=$v
done

The repository owner is then extracted using:

owner=${path%%/*}

and the repository name is extracted using:

repo=${path#*/}
repo=${repo%.git}

For example, if:

path=aont/foo.git

then:

owner=aont
repo=foo

Unless an explicit mapping exists, the owner is used directly as the GitHub CLI account:

account="$owner"

The corresponding PAT is then retrieved using:

token=$(gh auth token --user "$account")

Therefore, when accessing:

https://github.com/aont/foo.git

the command:

gh auth token --user aont

is automatically executed.

Similarly, for:

https://github.com/another-user/bar.git

the command:

gh auth token --user another-user

is executed.

There is no need to run gh auth switch every time.

Map Organization Owners to GitHub Accounts

The simple owner = account rule works well for repositories owned by personal accounts, but it does not necessarily work for organization-owned repositories.

For example, suppose the remote URL is:

https://github.com/my-org/foo.git

but the GitHub account that has access to the organization is:

my-work-account

In that case:

repository owner = my-org
authentication account = my-work-account

so executing:

gh auth token --user my-org

would be incorrect.

The helper can handle this with an owner-level mapping:

case "$owner" in
    my-org) account='my-work-account' ;;
    another-org) account='another-account' ;;
    *) account="$owner" ;;
esac

This means:

github.com/my-org/foo
        ↓
owner = my-org
        ↓
account = my-work-account
        ↓
gh auth token --user my-work-account

Any owner that is not explicitly listed continues to use the original behavior:

account = owner

so personal repositories do not require any additional configuration.

Override the Account for a Specific Repository

Sometimes repositories under the same organization need to use different accounts.

For example:

https://github.com/my-org/foo.git
https://github.com/my-org/special-repo.git

might normally use:

my-work-account

but special-repo might need:

special-account

The helper handles this by checking the full owner/repo combination before checking the owner:

case "$owner/$repo" in
    my-org/special-repo) account='special-account' ;;
    *)
        case "$owner" in
            my-org) account='my-work-account' ;;
            another-org) account='another-account' ;;
            *) account="$owner" ;;
        esac
        ;;
esac

The precedence is therefore:

repository-specific mapping
        ↓
organization/owner mapping
        ↓
owner name as the default account

For example:

github.com/my-org/special-repo
        ↓
special-account

github.com/my-org/other-repo
        ↓
my-work-account

github.com/aont/foo
        ↓
aont

This makes it possible to handle personal repositories, organization-owned repositories, and repository-specific exceptions with the same credential helper.

Reset Existing Credential Helpers with helper =

Another important point is:

helper =

On Git for Windows, for example, Git Credential Manager or gh auth git-credential might already be configured.

If another helper returns the credentials first, this helper might not be executed.

As a result, you might encounter an error like:

remote: Permission to foo/bar.git denied to wrong-account.

Therefore, use:

helper =
helper = "!f() { ... }; f"

The first empty helper = resets any previously configured credential helpers, and then only this helper is registered.

This is crucial when dealing with multiple GitHub accounts.

Use the Same Mechanism for Gists

The same credential helper can be configured for Gists.

However, the standard Gist clone URL is:

https://gist.github.com/GIST_ID.git

and the username cannot be determined from the URL.

Therefore, in this setup, the Gist remote URL is intentionally formatted as:

https://gist.github.com/USER/GIST_ID.git

For example, if:

https://gist.github.com/aont/0123456789abcdef.git

then, from the credential helper’s perspective:

path=aont/0123456789abcdef.git

so the command:

gh auth token --user aont

is automatically used.

This allows GitHub repositories and Gists to follow a similar rule:

github.com/OWNER/REPO
gist.github.com/USER/GIST
                ↓
       account is determined
                ↓
gh auth token --user ACCOUNT

For normal personal repositories and Gists, the owner or user is used directly. For organization-owned repositories, explicit mappings can override that behavior.

Verification

You can check which credentials Git is actually retrieving using git credential fill.

For example, run:

printf '%s\n' \
  'protocol=https' \
  'host=github.com' \
  'path=aont/foo.git' \
  '' |
git credential fill

The expected output is:

protocol=https
host=github.com
username=aont
password=...

You can also verify an organization mapping:

printf '%s\n' \
  'protocol=https' \
  'host=github.com' \
  'path=my-org/foo.git' \
  '' |
git credential fill

With the configuration above, the expected username is:

username=my-work-account

For the repository-specific override:

printf '%s\n' \
  'protocol=https' \
  'host=github.com' \
  'path=my-org/special-repo.git' \
  '' |
git credential fill

the expected username is:

username=special-account

If the username is unexpected, you can check whether another credential helper is still present using:

git config --show-origin --get-all credential.helper

Why Not Use gh auth git-credential Directly?

The GitHub CLI has a standard command:

gh auth git-credential

This is sufficient for normal single-account usage.

However, when using multiple accounts with the same host (github.com), you might want to explicitly control which gh account is used based on the repository owner or repository itself.

This helper uses the following routing rule:

remote URL
  ↓
owner/repository is extracted
  ↓
repository-specific mapping, if any
  ↓
owner mapping, if any
  ↓
otherwise use owner as account
  ↓
gh auth token --user account

As a result, you do not need to be aware of which account is currently active in gh.

Advantages of This Configuration

This method does not save the PAT itself in the .gitconfig file.

The PAT is retrieved each time it is needed using:

gh auth token --user ACCOUNT

Therefore, the configuration only saves the rule for determining which account to use.

Additionally, you do not need to run:

gh auth switch

or set:

git config credential.username ...

for each repository.

The remote URL itself becomes the input for credential routing.

For most personal repositories, no explicit configuration is necessary:

github.com/aont/foo
        ↓
account = aont

Organization accounts can be mapped:

github.com/my-org/foo
        ↓
account = my-work-account

and individual repositories can override that mapping:

github.com/my-org/special-repo
        ↓
account = special-account

Summary

When using multiple GitHub accounts over HTTPS, the repository path can be used to automatically select the appropriate gh account.

The mechanism is:

Git remote URL
  ↓
credential.useHttpPath
  ↓
path=owner/repo.git
  ↓
owner and repo are extracted
  ↓
repository-specific mapping?
  ├─ yes → use mapped account
  └─ no
       ↓
     owner mapping?
       ├─ yes → use mapped account
       └─ no → use owner as account
  ↓
gh auth token --user account
  ↓
username/password is returned to Git

The default rule remains simple:

repository owner = GitHub account

but explicit mappings make the same approach usable for organization-owned repositories where:

repository owner != account used for authentication

Repository-specific overrides can also handle exceptions within the same organization.

This keeps the credential-selection logic entirely inside .gitconfig, without storing PATs directly or requiring external scripts.


Edit page
Share this post:

Comments


Previous Post
What is the "Alerts" Syntax You Can Use in GitHub Markdown?
Next Post
How to Temporarily Change the Keyboard Layout