TL;DR
- Switching between work and personal GitHub accounts requires three layers of configuration.
- Combining
includeIf+insteadOf+ aghfunction wrapper delivers full automation. - Just
cdinto~/work/and everything switches over to the work account.
January 2026 update: I revised the Layer 3
ghcommand switching scheme. The previouschpwdhook +gh auth switchapproach has been replaced with a function wrapper +GH_TOKENenvironment 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:
- Forgetting to switch: Committing to a personal repo with the work email.
- Operational friction: Manually toggling
git configand SSH keys every time is tedious. - The gh CLI: Not just Git — GitHub CLI (
gh) needs its own switching too.
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:
- Git commit identity (
user.name,user.email) - SSH key (GitHub authentication)
- GitHub CLI (
ghcommand) account
Options Considered
Option A: Manual configuration each time
Manually set git config user.email per repository.
Pros:
- No additional setup needed.
Cons:
- Configuration required every time you create a repo.
- Forget to set it, and you commit with the wrong account.
- SSH keys and gh CLI still need separate management.
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:
- Unified management via environment variables.
- direnv is widely adopted.
Cons:
- Requires installing direnv.
- Tokens have to be written to a file (security concern).
- Each project needs its own
.envrc.
Option C: includeIf + insteadOf + gh function wrapper (chosen)
Combine standard Git features with shell functions.
Pros:
- No extra tools (uses Git/Zsh built-ins).
- Configure once; everything is automatic afterward.
- No need to write tokens to a file.
Cons:
- Many configuration items (3 layers).
- Requires understanding the mechanism.
Comparison Table
| Aspect | Manual | direnv | includeIf+α (chosen) |
|---|---|---|---|
| Initial setup effort | None | Medium | High |
| Daily friction | High | Low | None |
| Extra tools | None | direnv | None |
| Security | - | Token stored in file | OS keychain |
| Risk of forgetting | High | Low | None |
| Parallel work across windows | - | Supported | Supported |
Final Decision
Adopted: includeIf + insteadOf + gh function wrapper
Deciding factors:
- No additional tools: Achievable with Git and Zsh built-ins alone.
- No forgetting: Determined automatically by directory structure.
- Security: gh CLI tokens are stored in the OS keychain.
- Multi-window support: Per-process auth via environment variables, so other terminals are unaffected.
Trade-offs accepted:
- Initial setup is more involved (covered in this article).
- Without understanding the mechanism, troubleshooting is harder.
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.
| Layer | When used | What it identifies |
|---|---|---|
| Layer 1 | At git commit | Commit author |
| Layer 2 | At git push/pull | Connection auth to GitHub |
| Layer 3 | At 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
[includeIf "gitdir:~/work/"]: Applies when the current repo is under~/work/.path = ~/.gitconfig-work: Loads this config file.
Important details:
- The path after
gitdir:must end with/(it’s~/work/, not~/work). - The trailing
/is interpreted as**(any subdirectory). - Settings loaded later take precedence (which is why
user.nameis overridden by the work value).
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
Host github.com: Settings used when connecting togithub.com.Host github-work: Defines a fictional host name calledgithub-work.- The actual destination is
HostName github.com(real GitHub). - But the SSH key used is
id_ed25519_work(the work key).
- The actual destination is
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
insteadOf: Git auto-substitutes the URL during resolution.- For example,
git clone git@github.com:org/repo.git:- Internally becomes
git@github-work:org/repo.git. - SSH config then connects
github-workusing the work SSH key.
- Internally becomes
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.
| Element | Description |
|---|---|
gh auth token --user | Fetches the token for the specified account. |
GH_TOKEN="..." command gh | Passes the env var only to that command (process-local). |
command gh | Calls 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.
- Old approach:
gh auth switchmutates global state, so switching in one window affected the other. - New approach:
GH_TOKENis only set for the duration of that command (process-local), so other windows are unaffected.
Why account names go in ~/.zshrc.local:
- Account names are personal, so I don’t want them in my dotfiles repository.
~/.zshrc.localis treated as a Git-untracked file.
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
- Zero forgotten switches: Determined automatically by directory structure, no need to think about it.
- No setup overhead at the start of work: Previously I’d always check “which account am I on?”
- Fewer incidents: No more accidental commits/pushes from the wrong account.
- Parallel work across windows now works: Thanks to the function wrapper approach, work and personal terminals can stay open side by side.
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:
- The env var is only effective during command execution (process-local).
- No
cdhook is needed; it’s simpler. - Global state (
gh auth status) is never mutated.
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
- gh auth token - GitHub CLI Manual
- Using the GitHub CLI across GitHub platforms - GitHub Docs
- git-config - includeIf - Git Documentation
Related articles
- Log in to multiple GitHub accounts with the CLI - GitHub Changelog
- Git config with multiple identities - GitHub Gist