Skip to main content

Auto-Switch Git and GitHub CLI Accounts Just by cd

Cover image for Auto-Switch Git and GitHub CLI Accounts Just by cd

TL;DR

January 2026 update: I revised the Layer 3 gh command switching scheme. The previous chpwd hook + gh auth switch approach has been replaced with a function wrapper + GH_TOKEN environment variable approach. This now handles parallel work across multiple terminal windows.

Background and Problem

The problem I wanted to solve

“I want to use separate work and personal GitHub accounts.”

This is a common challenge. Manual switching works, but it has issues:

The ideal state

“Just change directories, and everything switches automatically.”

Specifically: when I move under ~/work/ it should be the work account; otherwise, personal. All of the following should switch automatically:

Options Considered

Option A: Manual configuration each time

Manually set git config user.email per repository.

Pros:

Cons:

Option B: direnv + GH_TOKEN

Use direnv to set environment variables per directory.

# ~/work/.envrc
export GH_TOKEN="ghp_xxxx"
export GIT_AUTHOR_EMAIL="work@example.com"

Pros:

Cons:

Option C: includeIf + insteadOf + gh function wrapper (chosen)

Combine standard Git features with shell functions.

Pros:

Cons:

Comparison Table

AspectManualdirenvincludeIf+α (chosen)
Initial setup effortNoneMediumHigh
Daily frictionHighLowNone
Extra toolsNonedirenvNone
Security-Token stored in fileOS keychain
Risk of forgettingHighLowNone
Parallel work across windows-SupportedSupported

Final Decision

Adopted: includeIf + insteadOf + gh function wrapper

Deciding factors:

  1. No additional tools: Achievable with Git and Zsh built-ins alone.
  2. No forgetting: Determined automatically by directory structure.
  3. Security: gh CLI tokens are stored in the OS keychain.
  4. Multi-window support: Per-process auth via environment variables, so other terminals are unaffected.

Trade-offs accepted:

Why “Three Layers” Are Necessary

Here’s the key point. Switching Git and GitHub accounts requires three distinct configurations.

┌─────────────────────────────────────────────────────┐
│                   Your Machine                      │
├─────────────────────────────────────────────────────┤
│                                                     │
│  [Layer 1] Git user settings                        │
│    → Name and email recorded in commits             │
│    → Switched via includeIf in .gitconfig           │
│                                                     │
│  [Layer 2] SSH key                                  │
│    → Authentication for git push/pull to GitHub     │
│    → Switched via SSH host alias + insteadOf        │
│                                                     │
│  [Layer 3] gh CLI account                           │
│    → Account used for gh pr create and other ops    │
│    → Switched via gh function wrapper + GH_TOKEN    │
│                                                     │
└─────────────────────────────────────────────────────┘


              ┌─────────────────────┐
              │      GitHub         │
              │ (personal or work)  │
              └─────────────────────┘

Why each layer needs its own configuration

Each one is invoked at a different moment.

LayerWhen usedWhat it identifies
Layer 1At git commitCommit author
Layer 2At git push/pullConnection auth to GitHub
Layer 3At gh pr create, etc.The actor for GitHub API operations

Configuring only Layer 1, for example, still leaves you pushing with the wrong SSH key. Full switching only works once all three layers are set up correctly.

How to Configure Each Layer

The example below treats ~/work/ as work and everything else as personal.

Layer 1: Auto-switching user settings via includeIf

includeIf is a Git feature that loads a different config file based on a condition.

~/.gitconfig (the main config file)

# Default (personal) settings
[user]
    name = Your Personal Name
    email = personal@example.com

# Under ~/work/, additionally load the work config
[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work

~/.gitconfig-work (the work config file)

[user]
    name = Your Work Name
    email = work@company.com

How it works

Important details:

Verification

# Check from a personal directory
cd ~/personal/some-repo
git config user.email
# → personal@example.com

# Check from a work directory
cd ~/work/some-project
git config user.email
# → work@company.com

To confirm exactly which file each setting comes from:

git config --list --show-origin

This shows the source file for every setting.

Layer 2: SSH host alias + insteadOf for SSH key switching

SSH key switching combines two pieces of configuration.

Step 1: Define a host alias in the SSH config

Add the following to ~/.ssh/config:

# Personal (default)
Host github.com
    IdentityFile ~/.ssh/id_ed25519_personal

# Work (alias)
Host github-work
    HostName github.com
    IdentityFile ~/.ssh/id_ed25519_work

How it works

So accessing git@github-work:org/repo.git uses the work SSH key.

Step 2: Auto-rewrite URLs with Git’s insteadOf

For work repos, automatically rewrite github.com access to github-work.

Add the following to ~/.gitconfig-work:

[user]
    name = Your Work Name
    email = work@company.com

# Rewrite SSH-form URLs
[url "git@github-work:"]
    insteadOf = git@github.com:

# Rewrite HTTPS-form URLs too
[url "git@github-work:"]
    insteadOf = https://github.com/

How it works

Verification

# Check the remote URL in a work repo
cd ~/work/some-project
git remote -v
# → origin  git@github.com:company/repo.git (fetch)
#    ↑ The displayed value is still github.com

# Check the URL actually used
git config --get-regexp 'url.*'
# → url.git@github-work:.insteadof git@github.com:

Layer 3: gh CLI account switching via a gh function wrapper

GitHub CLI (gh) gained multi-account support in v2.40.0.

Prerequisite: log in with both accounts

# Log in with the first account
gh auth login

# Log in with the second account (added)
gh auth login

# Check login status
gh auth status

gh auth status lists every account you’re logged into and which one is currently active.

Add to ~/.zshrc

########################################
# GitHub CLI account switching (function wrapper approach)
# Work account under ~/work, personal account elsewhere.
# Set the account names in ~/.zshrc.local:
#   GH_PERSONAL_ACCOUNT="your-personal"
#   GH_WORK_ACCOUNT="your-work"
########################################
gh() {
  local token
  if [[ "$PWD" == "$HOME/work"* ]]; then
    token=$(command gh auth token --user "$GH_WORK_ACCOUNT" 2>/dev/null)
  else
    token=$(command gh auth token --user "$GH_PERSONAL_ACCOUNT" 2>/dev/null)
  fi

  if [[ -n "$token" ]]; then
    GH_TOKEN="$token" command gh "$@"
  else
    command gh "$@"
  fi
}

Set account names in ~/.zshrc.local

# Set your GitHub usernames
GH_PERSONAL_ACCOUNT="your-personal-username"
GH_WORK_ACCOUNT="your-work-username"

How it works

What the function wrapper does:

It wraps the gh command in a shell function that, at invocation time, checks the current directory and uses the appropriate token.

ElementDescription
gh auth token --userFetches the token for the specified account.
GH_TOKEN="..." command ghPasses the env var only to that command (process-local).
command ghCalls the actual gh command, not the function.

Why the command keyword matters:

command gh "$@"

Inside a shell function, calling gh would recursively call the function itself. The command keyword bypasses the function and invokes the real binary.

Why this approach:

I previously used a chpwd hook + gh auth switch, but it broke when working across multiple terminal windows in parallel.

Why account names go in ~/.zshrc.local:

Verification

# Check from a work directory
cd ~/work/some-project
gh api user --jq '.login'
# → work-username

# Check from a personal directory
cd ~/personal/my-repo
gh api user --jq '.login'
# → personal-username

Note: gh auth status shows global state and does not accurately reflect the current state with the function wrapper approach. To check which account is actually used, use gh api user as shown above.

End-to-End Verification

Once everything is configured, verify with these steps:

# 1. Check from a personal directory
cd ~/personal/my-repo
git config user.email          # → personal@example.com
gh api user --jq '.login'      # → personal-username

# 2. Check from a work directory
cd ~/work/company-project
git config user.email          # → work@company.com
gh api user --jq '.login'      # → work-username

# 3. Confirm push works (in a work repo)
git push --dry-run             # Authenticates with the work SSH key

Results After Adoption

What worked

How I got here

I originally adopted the chpwd hook + gh auth switch approach, but it broke when working in parallel across multiple terminal windows. Because gh auth switch mutates global state, switching in one window affected the other.

So I moved to the function wrapper + GH_TOKEN environment variable approach. With this approach:

What I Learned

1. Git auth and GitHub auth are different things

It’s tempting to think “just change Git settings and you’re done,” but in reality there are multiple layers — SSH auth, GitHub API auth, and so on. Each needs to be understood and configured.

2. Built-in features are often enough

includeIf, insteadOf, and shell functions are all standard features. Without adding new tools, I achieved the goal by combining what was already there.

3. Pursuing UX is worth it

I obsessed over the “just cd” experience. Setup is complex, but day-to-day operations stay simple. Balancing upfront investment against ongoing cost is the call worth making.

References

Official documentation

ZSL
ZSL

AI Engineer

Researching and practicing development workflows powered by Generative AI.