Skip to content

Instantly share code, notes, and snippets.

@gwpl
Last active March 5, 2026 20:06
Show Gist options
  • Select an option

  • Save gwpl/9ee9b6307d79ac2567b480a49186078b to your computer and use it in GitHub Desktop.

Select an option

Save gwpl/9ee9b6307d79ac2567b480a49186078b to your computer and use it in GitHub Desktop.
GitHub CLI (gh) Multi-Account Usage: Parallel-Safe & Secure (GH_CONFIG_DIR approach)

Understanding SSH Host Aliases ("Pseudo-Domains") for Git Multi-Account

The Core Concept

When you see github.com-work in an SSH config or git remote URL, that is NOT a real domain name. It's an SSH host alias (sometimes called a "pseudo-domain") — a label that only exists in your ~/.ssh/config file. It never hits DNS.

# This is an ALIAS — "github.com-work" is a made-up name
Host github.com-work
    HostName github.com    # ← the REAL server to connect to
    User git
    IdentityFile ~/.ssh/id_ed25519_work  # ← the key to use

When SSH sees github.com-work, it:

  1. Looks it up in ~/.ssh/config
  2. Finds the real hostname (github.com)
  3. Uses the specified key (id_ed25519_work)
  4. Connects to github.com authenticating with that specific key

Why This Is Necessary

GitHub identifies which account you are by your SSH key, not by the URL.

Without aliases, SSH picks a key automatically (usually the default or first one offered). If you have multiple GitHub accounts, the default key maps to only one of them — the others will fail.

The alias trick forces SSH to use a specific key for a specific "nickname", even though all nicknames point to the same real server (github.com).

The Full Flow: What Happens When You git push

git push origin main
  │
  ▼
git reads remote URL: git@github.com-work:org/repo.git
  │
  ▼
git invokes SSH: ssh git@github.com-work
  │
  ▼
SSH reads ~/.ssh/config → finds "Host github.com-work"
  │
  ▼
SSH connects to HostName: github.com (the REAL server)
  with IdentityFile: ~/.ssh/id_ed25519_work
  │
  ▼
GitHub receives the SSH key → identifies you as "work-user"
  │
  ▼
GitHub checks: does "work-user" have access to org/repo?
  │
  ▼
Push proceeds (or fails with "Repository not found" — see Troubleshooting below)

As a Mermaid Diagram

sequenceDiagram
    participant Git as git push
    participant SSH as SSH client
    participant Config as ~/.ssh/config
    participant GH as github.com

    Git->>SSH: connect to github.com-work
    SSH->>Config: lookup "Host github.com-work"
    Config-->>SSH: HostName=github.com, Key=id_ed25519_work
    SSH->>GH: connect to github.com using id_ed25519_work
    GH-->>SSH: authenticated as work-user
    SSH-->>Git: connection established, push proceeds
Loading

The Mental Model

Think of it as giving each GitHub account a nickname that SSH understands:

Alias (nickname) Real server SSH key used GitHub account
github.com github.com id_ed25519_personal personal-user
github.com-work github.com id_ed25519_work work-user
github.com-oss github.com id_rsa_oss oss-user

All three connect to the same server. The alias is just a routing label.

Common Source of Confusion

GitHub's web UI shows clone URLs like:

git@github.com:org/repo.git

This uses bare github.com — which means your default SSH key. If your default key belongs to personal-user but the repo belongs to work-user, the push will fail.

The fix: Replace github.com with your SSH alias in the remote URL:

# Wrong (uses default key):
git@github.com:org/repo.git

# Correct (uses work key via alias):
git@github.com-work:org/repo.git

How This Connects to the gh CLI

This setup involves two separate authentication layers that must both be configured:

flowchart TB
    subgraph "Layer 1: API Operations — gh CLI"
        A["gh-work pr create"] --> B["GH_CONFIG_DIR=~/.config/gh-work"]
        B --> C["OAuth token for work-user"]
        C --> D["GitHub REST/GraphQL API"]
    end

    subgraph "Layer 2: Git Transport — SSH"
        E["git push origin main"] --> F["Remote: git@github.com-work:org/repo"]
        F --> G["~/.ssh/config → Host github.com-work"]
        G --> H["~/.ssh/id_ed25519_work"]
        H --> I["GitHub SSH server"]
    end

    D -.- |"Both must authenticate as the SAME account"| I
Loading
  • gh-work alias (via GH_CONFIG_DIR) controls which account the gh CLI uses for API calls (PRs, issues, releases, etc.)
  • github.com-work alias (via ~/.ssh/config) controls which SSH key git uses for transport (push, pull, fetch, clone)

If you configure one but not the other, some operations work and others fail in confusing ways:

Misconfiguration Symptom
Wrong GH_CONFIG_DIR gh commands create PRs/issues on wrong account (or 404)
Wrong remote URL (bare github.com) git push fails with "Repository not found"
Both wrong Everything fails

Troubleshooting

"ERROR: Repository not found" — The Misleading Error

If you get this error when pushing to a repo you know exists, the most likely cause is your SSH key mapped to the wrong account.

GitHub returns "not found" (instead of "access denied") for security — it prevents leaking whether private repos exist. But this means wrong SSH key looks identical to wrong repo URL.

Debugging Commands

# Which GitHub user does this SSH alias authenticate as?
ssh -T git@github.com-ACCOUNT
# Expected: "Hi USERNAME! You've successfully authenticated..."

# Which SSH key is being offered? (verbose mode)
ssh -vT git@github.com-ACCOUNT 2>&1 | grep "Offering public key"

# What remote URL does this repo use?
git remote -v
# Check: does it show github.com-ACCOUNT or bare github.com?

# Which gh account is active for this config dir?
GH_CONFIG_DIR=~/.config/gh-ACCOUNT gh auth status

Quick Diagnosis Flowchart

git push fails with "Repository not found"
  │
  ├─ Run: ssh -T git@github.com-ACCOUNT
  │   ├─ Shows wrong username → SSH key/config issue
  │   └─ Shows correct username → repo permissions issue
  │
  └─ Run: git remote -v
      ├─ Shows bare github.com → fix with: git remote set-url origin git@github.com-ACCOUNT:org/repo.git
      └─ Shows github.com-ACCOUNT → SSH config or key issue

GitHub CLI (gh) Multi-Account Usage: Parallel-Safe & Secure

The Problem

When working with multiple GitHub accounts (e.g., personal + work + OSS org), you need:

  1. API operations (gh pr, gh issue, etc.) to use the correct account
  2. Git transport (clone/push/pull) to use the correct SSH key
  3. Parallel safety — multiple scripts running simultaneously must not interfere
  4. Security — tokens must not leak via /proc/$PID/environ, ps auxe, etc.

SSH Config for Git Transport

Use SSH host aliases in ~/.ssh/config to route git operations per-account:

# Default account
Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_personal

# Work account
Host github.com-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work

# OSS account
Host github.com-oss
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_rsa_oss

Usage:

git clone git@github.com-work:org/repo.git

Two Separate Auth Layers — Both Must Be Configured

Key insight: This multi-account setup involves two independent authentication systems that must both point to the same account. Misconfiguring one while the other is correct causes confusing partial failures.

flowchart TB
    subgraph "Layer 1: API Operations — gh CLI"
        A["gh-work pr create"] --> B["GH_CONFIG_DIR=~/.config/gh-work"]
        B --> C["OAuth token for work-user"]
        C --> D["GitHub REST/GraphQL API"]
    end

    subgraph "Layer 2: Git Transport — SSH"
        E["git push origin main"] --> F["Remote: git@github.com-work:org/repo"]
        F --> G["~/.ssh/config → Host github.com-work"]
        G --> H["~/.ssh/id_ed25519_work"]
        H --> I["GitHub SSH server"]
    end

    D -.- |"Both must authenticate as the SAME account"| I
Loading
Misconfiguration Symptom
Wrong GH_CONFIG_DIR gh commands target wrong account or get 404
Wrong remote URL (bare github.com) git push fails with "Repository not found"
Both wrong Everything fails

For a deep explanation of how SSH host aliases work under the hood, see the companion file: bb_HOW_SSH_HOST_ALIASES_WORK.md


gh CLI Multi-Account: Built-in Support (since v2.40.0)

Adding accounts

gh auth login   # first account
gh auth login   # second account — additive, doesn't replace
gh auth status  # shows all accounts

Switching (NOT parallel-safe)

gh auth switch                     # interactive
gh auth switch --user myworkuser   # direct

Warning: gh auth switch mutates global state (~/.config/gh/hosts.yml). If two scripts call gh auth switch concurrently, they will race.

Approach 1: GH_TOKEN (Simple but Leaks to /proc)

GH_TOKEN=$(gh auth token --user workuser) gh pr list --repo org/repo

Security concern

Environment variables are visible to the same UID via:

cat /proc/$PID/environ | tr '\0' '\n'
ps auxe | grep gh

GH_TOKEN contains the actual secret token — if leaked, it grants full API access with whatever scopes were configured.

Verdict: Fine for single-user dev machines with no untrusted processes. Risky on shared/multi-tenant systems.

Approach 2: GH_CONFIG_DIR (Recommended — Parallel-Safe & Secure)

⚠️ CRITICAL: When running gh auth login for each config dir, always select SSH as the git protocol. This ensures gh and git use the same SSH host aliases from ~/.ssh/config, avoids HTTPS credential helper conflicts, and is essential for submodule support across multiple accounts. See the companion guide for details.

Instead of putting a token in the environment, point gh at a separate config directory per account. The env var contains only a filesystem path, not a secret.

One-time setup

# Create per-account config directories
mkdir -p ~/.config/gh-personal
mkdir -p ~/.config/gh-work

# Login each account into its own config dir
# ⚠️ Select SSH as git protocol when prompted!
GH_CONFIG_DIR=~/.config/gh-personal gh auth login
GH_CONFIG_DIR=~/.config/gh-work gh auth login

Usage in scripts

# Script A — personal account
export GH_CONFIG_DIR=~/.config/gh-personal
gh pr list --repo myuser/myrepo

# Script B — work account (parallel, no conflict)
export GH_CONFIG_DIR=~/.config/gh-work
gh issue create --repo org/repo --title "Bug"

Convenience aliases

# In ~/.bashrc or ~/.zshrc
alias gh-personal='GH_CONFIG_DIR=~/.config/gh-personal gh'
alias gh-work='GH_CONFIG_DIR=~/.config/gh-work gh'

Then:

gh-work pr list --repo org/repo
gh-personal issue list --repo myuser/myrepo

⚠️ Critical Gotcha: gh repo create/clone and Remote URLs

When you run:

gh-work repo create org/repo --private --source=. --push
# or
gh-work repo clone org/repo

The gh CLI sets the git remote to git@github.com:org/repo.git — using bare github.com, NOT your SSH alias github.com-work. The gh CLI has no knowledge of your SSH host aliases.

You must fix the remote URL after every gh create or clone:

git remote set-url origin git@github.com-work:org/repo.git

Without this, git push/pull will use your default SSH key (wrong account), while gh-work API commands work fine — a confusing split-brain situation.

Wrapper function to automate this:

# Add to ~/.bashrc or ~/.zshrc
gh-work-clone() {
    GH_CONFIG_DIR=~/.config/gh-work gh repo clone "$1" "${2:-.}"
    local dir="${2:-$(basename "$1")}"
    git -C "$dir" remote set-url origin "git@github.com-work:${1}.git"
    echo "✓ Fixed remote to use github.com-work SSH alias"
}

Why this is better

Aspect GH_TOKEN GH_CONFIG_DIR
/proc/$PID/environ exposure Leaks secret token Only leaks a path (not secret)
ps auxe exposure Leaks secret token Only leaks a path
Parallel-safe Yes Yes
Token storage In memory/env In keyring or 0600 file
Setup complexity Low Medium (one-time)

Where tokens actually live

With GH_CONFIG_DIR, tokens are stored:

  1. System keyring (best) — encrypted, managed by OS
  2. $GH_CONFIG_DIR/hosts.yml (fallback) — plaintext but chmod 0600

Check which storage is used:

GH_CONFIG_DIR=~/.config/gh-work gh auth status
# Look for "(keyring)" vs file path

Extracting a token for other tools

If you need the token for non-gh tools (e.g., curl):

# Read from the config dir's auth — stays in the pipe, not in env
GH_CONFIG_DIR=~/.config/gh-work gh auth token | some-tool --token-stdin

Or if the tool doesn't support stdin:

# Slightly less safe but scoped to one command
GH_CONFIG_DIR=~/.config/gh-work gh auth token | xargs -I{} curl -H "Authorization: token {}" ...

Complete workflow example

#!/usr/bin/env bash
# deploy.sh — uses work account, parallel-safe

export GH_CONFIG_DIR=~/.config/gh-work

REPO="org/service"

# Create release
gh release create "v1.2.3" --repo "$REPO" --title "v1.2.3" --notes "Bug fixes"

# Check CI status
gh run list --repo "$REPO" --limit 5

Manual Token Authentication (Browser-Based)

Instead of using gh auth login's browser OAuth flow, you can manually create a Personal Access Token (PAT) at https://github.com/settings/tokens and paste it:

GH_CONFIG_DIR=~/.config/gh-ACCOUNT_NAME gh auth login --with-token <<< "ghp_YOUR_TOKEN_HERE"

Classic vs Fine-Grained Tokens: Which to Choose?

GitHub offers two token types. The choice depends on your access needs:

Aspect Classic (PAT) Fine-Grained (PAT)
Repository scope All repos you can access Per-repo or per-owner selection
Permission model Broad scopes (repo, admin:org, etc.) Granular per-resource permissions
Expiration Optional (can be non-expiring) Required (max 366 days in orgs)
Org admin control None — org admins have no visibility Org admins can require approval, set policies
Cross-org access Single token works across orgs Scoped to one user/org per token
Outside collaborator ✅ Works ❌ Cannot access org repos as outside collaborator
Public repo write (non-member) ✅ Works ❌ No write access to repos you don't own/aren't a member of
REST API coverage Full Most endpoints, but some classic-only gaps remain

When to choose Classic:

  • You need a single token across multiple organizations
  • You're an outside collaborator on org repos
  • You need write access to public repos you don't own
  • You want a non-expiring token for long-running automation
  • You need endpoints not yet supported by fine-grained tokens

When to choose Fine-Grained:

  • You want least-privilege access (only specific repos, only specific permissions)
  • Your organization requires token approval workflows
  • You want automatic expiration as a safety net
  • You prefer per-repo audit trails
  • You're working within a single org/user scope

Note: GitHub recommends fine-grained tokens as the more secure default. However, classic tokens remain necessary for certain cross-org and collaborator workflows. Evaluate based on your actual access patterns.

Reference: GitHub Docs — Managing personal access tokens

Troubleshooting

"ERROR: Repository not found" — The #1 Gotcha

If git push fails with this error for a repo you know exists, the cause is almost always wrong SSH key (wrong account). GitHub returns "not found" instead of "access denied" for security reasons (prevents repo enumeration). This is extremely misleading.

Fix: Ensure your remote URL uses the correct SSH alias, not bare github.com:

# Check current remote
git remote -v

# Fix it
git remote set-url origin git@github.com-ACCOUNT:org/repo.git

Debugging Commands Cheat Sheet

# 1. Which GitHub user does this SSH alias authenticate as?
ssh -T git@github.com-ACCOUNT
# Expected: "Hi USERNAME! You've successfully authenticated..."

# 2. Which SSH key is being offered?
ssh -vT git@github.com-ACCOUNT 2>&1 | grep "Offering public key"

# 3. What remote URL does this repo use?
git remote -v
# Look for: github.com-ACCOUNT (good) vs github.com (bad)

# 4. Which gh account is active for this config dir?
GH_CONFIG_DIR=~/.config/gh-ACCOUNT gh auth status

Automation / AI Agent Guardrails

When using AI coding assistants (Claude Code, Cursor, Copilot, etc.) or CI scripts in a multi-account repo, they may default to bare gh (wrong account). Mitigations:

  • direnv / .envrc — auto-set GH_CONFIG_DIR per project directory:

    # .envrc in project root
    export GH_CONFIG_DIR=~/.config/gh-work
  • Project-level hooks — e.g., Claude Code PreToolUse hook that blocks bare gh and requires gh-work

  • Wrapper scripts — replace gh with a project-aware wrapper that auto-detects the account based on remote URL

Summary

  • For git operations: Use SSH host aliases (github.com-work) in ~/.ssh/config
  • For gh CLI operations: Use GH_CONFIG_DIR per-account (not GH_TOKEN)
  • Never use gh auth switch in automated/parallel workflows — it mutates global state
  • GH_CONFIG_DIR is the only approach that is both parallel-safe AND doesn't leak secrets to /proc
  • For manual token auth: Choose between Classic (broad, cross-org) and Fine-Grained (scoped, auditable) based on your needs

How to Add a New GitHub Account (SSH + gh CLI)

Step-by-step guide for adding a new GitHub account to your local machine, covering both git transport (SSH) and API operations (gh CLI).

Prerequisites

  • ssh-keygen available (standard on Linux/macOS)
  • gh CLI installed (v2.40.0+ for multi-account support)
  • Access to the GitHub web UI for the new account (to upload the SSH key)

Step 1: Generate a Dedicated SSH Key

Each GitHub account needs its own SSH key. We recommend ed25519:

# Replace ACCOUNT_NAME with a short identifier (e.g., work, oss, client)
ssh-keygen -t ed25519 -C "ACCOUNT_NAME@github.com" -f ~/.ssh/id_ed25519_ACCOUNT_NAME
  • You'll be prompted for a passphrase — choose based on your security needs
  • This creates two files:
    • ~/.ssh/id_ed25519_ACCOUNT_NAME (private key)
    • ~/.ssh/id_ed25519_ACCOUNT_NAME.pub (public key)

Step 2: Add SSH Config Entry (the "Pseudo-Domain")

Edit ~/.ssh/config and add:

Host github.com-ACCOUNT_NAME
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_ACCOUNT_NAME

This creates an SSH host aliasgithub.com-ACCOUNT_NAME is NOT a real domain, it's a label that tells SSH "when I say github.com-ACCOUNT_NAME, connect to github.com but use this specific key." Git remote URLs then use this alias (git@github.com-ACCOUNT_NAME:org/repo.git) to route through the correct key.

For a deep explanation of how this works under the hood, see the companion file: bb_HOW_SSH_HOST_ALIASES_WORK.md

Step 3: Upload Public Key to GitHub

Option A: Via GitHub Web UI

cat ~/.ssh/id_ed25519_ACCOUNT_NAME.pub

Copy the output, then:

  1. Log in to GitHub as the new account
  2. Go to Settings → SSH and GPG keys → New SSH key
  3. Paste the public key, give it a title (e.g., your hostname), and save

Option B: Via gh CLI (if already authenticated — see Step 4)

GH_CONFIG_DIR=~/.config/gh-ACCOUNT_NAME gh ssh-key add ~/.ssh/id_ed25519_ACCOUNT_NAME.pub --title "$(hostname)"

Step 4: Set Up gh CLI for the New Account

⚠️ IMPORTANT: Choose SSH as the git protocol when prompted.

During gh auth login, you will be asked:

? What is your preferred protocol for Git operations on this host?
  > SSH
    HTTPS

Always select SSH. Reasons:

  • Consistent with the SSH host aliases configured in Step 2
  • Works correctly with git submodules (submodule URLs like git@github.com-ACCOUNT_NAME:... resolve properly)
  • No credential helper conflicts between accounts
  • HTTPS credential caching is global and breaks multi-account setups
  • SSH keys are per-account by design — no ambiguity about which identity is used for git transport
# Create a dedicated config directory for this account
mkdir -p ~/.config/gh-ACCOUNT_NAME

# Login — select SSH when prompted for git protocol!
GH_CONFIG_DIR=~/.config/gh-ACCOUNT_NAME gh auth login

During the interactive login:

  1. GitHub.com (or Enterprise if applicable)
  2. SSH ← critically important, see note above
  3. Skip uploading a new SSH key (already done in Step 3, or do it here)
  4. Authenticate via browser or token

Step 5: Add Shell Aliases

Add to your ~/.bashrc (or ~/.zshrc):

# GitHub CLI aliases — per-account, parallel-safe
alias gh-ACCOUNT_NAME='GH_CONFIG_DIR=~/.config/gh-ACCOUNT_NAME gh'

Reload:

source ~/.bashrc  # or source ~/.zshrc

Now you can use:

gh-ACCOUNT_NAME pr list --repo org/repo
gh-ACCOUNT_NAME issue create --repo org/repo --title "Bug report"
gh-ACCOUNT_NAME repo clone org/repo

Step 6: Test Everything

Test SSH connection

ssh -T git@github.com-ACCOUNT_NAME
# Expected: "Hi USERNAME! You've successfully authenticated, but GitHub does not provide shell access."

Test gh CLI

gh-ACCOUNT_NAME auth status
# Should show the correct account and "(keyring)" for token storage

Test git clone via SSH alias

git clone git@github.com-ACCOUNT_NAME:org/repo.git

Step 7: Fix Remote URL After gh Clone or Create

This is critical. When you use gh-ACCOUNT_NAME repo clone or gh-ACCOUNT_NAME repo create --source=. --push, the gh CLI sets the remote to bare github.com — it has no knowledge of your SSH aliases.

# Check current remote
git remote -v
# If it shows git@github.com:org/repo.git (missing -ACCOUNT_NAME), fix it:

git remote set-url origin git@github.com-ACCOUNT_NAME:org/repo.git

Without this, gh-ACCOUNT_NAME API commands will work (correct GH_CONFIG_DIR), but git push/pull will fail with "Repository not found" because it uses the wrong SSH key.

Tip: The "Repository not found" error is misleading — GitHub returns "not found" instead of "access denied" for security. If you see this for a repo you know exists, check your remote URL first.

Step 8 (Optional): Verify SSH Protocol Is Set

Double-check that the gh config dir has SSH as the git protocol:

cat ~/.config/gh-ACCOUNT_NAME/hosts.yml

You should see:

github.com:
    git_protocol: ssh
    ...

If it shows https, fix it:

GH_CONFIG_DIR=~/.config/gh-ACCOUNT_NAME gh config set git_protocol ssh --host github.com

Quick Reference: Adding Another Account

Once you've done this once, adding more accounts is just:

# 1. Generate key
ssh-keygen -t ed25519 -C "NEWACCOUNT@github.com" -f ~/.ssh/id_ed25519_NEWACCOUNT

# 2. Add to ~/.ssh/config
cat >> ~/.ssh/config << 'EOF'

Host github.com-NEWACCOUNT
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_NEWACCOUNT
EOF

# 3. Upload key to GitHub (web UI or gh cli)
cat ~/.ssh/id_ed25519_NEWACCOUNT.pub

# 4. Setup gh CLI (SELECT SSH when prompted!)
mkdir -p ~/.config/gh-NEWACCOUNT
GH_CONFIG_DIR=~/.config/gh-NEWACCOUNT gh auth login

# 5. Add alias to ~/.bashrc
echo "alias gh-NEWACCOUNT='GH_CONFIG_DIR=~/.config/gh-NEWACCOUNT gh'" >> ~/.bashrc
source ~/.bashrc

# 6. Test
ssh -T git@github.com-NEWACCOUNT
gh-NEWACCOUNT auth status

Why SSH Over HTTPS for Multi-Account gh Setup

Aspect SSH HTTPS
Per-account identity SSH key per alias Global credential helper — conflicts
Submodules Work with github.com-ACCOUNT aliases Break with mixed credentials
Credential storage Key file + optional passphrase OS credential store (single entry per host)
gh + git alignment Both respect the same SSH config gh token and git credential can diverge
Parallel safety Each process uses its own key via alias Credential helper is shared mutable state
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment