Multiple GitHub Accounts with direnv
If you use more than one GitHub account — say, a personal account and a work account — you've probably hit the friction of switching between them. Wrong SSH key. Wrong gh auth token. A commit pushed to the wrong account. The usual advice involves git's includeIf conditional config or manually running gh auth switch every time you change context.
I wanted something that just works: cd into a directory and have the right GitHub identity loaded automatically. Here's how I set that up with direnv.
Table of Contents
The Problem
I have two GitHub accounts: lucasmccomb (personal) and a work account. Each has its own SSH key, its own gh auth token, and its own set of repos. The challenge is making sure that when I'm working in ~/code/work/, everything - git pushes, gh CLI commands, API calls - routes through the correct account without me thinking about it.
Git's built-in includeIf can handle some of this, but it only covers git config values (name, email, signing key). It doesn't touch the gh CLI, which uses its own auth system. I needed a solution that sets the entire shell environment per directory.
The Pieces
The setup has three layers:
- SSH host aliases — route
git pushto the correct SSH key - direnv
.envrcfiles — setGH_TOKENand other env vars per directory gh authwith multiple accounts — provide the tokens that direnv exports
SSH Config
SSH doesn't natively support "use this key for this directory." But it does support host aliases. By defining a custom hostname that maps to github.com with a specific key, you can control which identity git uses based on the remote URL.
# ~/.ssh/config
# Personal (lucasmccomb)
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519
# Work
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
The IdentitiesOnly yes line is important — it tells SSH to only offer the specified key, not every key in the agent. Without it, SSH might try your personal key first and succeed (or fail confusingly) before reaching the work key.
With this config, a repo cloned as git@github-work:org/repo.git will always use the work key, while [email protected]:lucasmccomb/repo.git uses the personal key. The hostname in the remote URL is what determines the identity.
direnv
direnv is a shell extension that loads and unloads environment variables based on .envrc files as you navigate directories. It's the glue that makes all of this automatic.
At the workspace root (~/code/), I have an .envrc that sets the default (personal) account:
# ~/code/.envrc
export GH_TOKEN="$(gh auth token --user lucasmccomb)"
export GH_ACCOUNT="lucasmccomb"
In the work directory, a second .envrc overrides it:
# ~/code/work/.envrc
export GH_TOKEN="$(gh auth token --user work-username)"
export GH_ACCOUNT="work-username"
direnv inherits from parent directories, so subdirectories of ~/code/work/ get the work token, while everything else under ~/code/ gets the personal token.
The GH_TOKEN environment variable is what the gh CLI and GitHub API clients check first — before any stored auth. By exporting it, every gh pr create, gh issue list, or API call in that shell session uses the correct account.
GH_ACCOUNT is a custom variable I set for my own tooling. Some of my scripts and hooks reference it to know which account context they're running in.
gh CLI Multi-Account Auth
The gh CLI supports multiple authenticated accounts. You register each one:
gh auth login --hostname github.com --user lucasmccomb
gh auth login --hostname github.com --user work-username
Once both are registered, gh auth token --user <account> returns the token for that specific account - which is exactly what the .envrc files call.
How It Comes Together
Here's what happens when I open a terminal and navigate:
~/ $ cd code/work/my-project
direnv: loading ~/code/work/.envrc
direnv: export +GH_ACCOUNT +GH_TOKEN
~/code/work/my-project $ gh api user --jq .login
work-username
~/code/work/my-project $ cd ~/code/lem-fyi-repos/lem-fyi-0
direnv: loading ~/code/.envrc
direnv: export ~GH_ACCOUNT ~GH_TOKEN
~/code/lem-fyi-repos/lem-fyi-0 $ gh api user --jq .login
lucasmccomb
No manual switching. No remembering which account is active. The directory determines the identity.
For git operations, the SSH host alias in the remote URL handles key selection independently. So git push in a work repo uses the work SSH key, and gh pr create in the same repo uses the work token - both automatically.
Setup Steps
If you want to replicate this:
1. Generate a separate SSH key for each account:
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_work
2. Add each public key to the corresponding GitHub account (Settings > SSH Keys).
3. Configure SSH host aliases in ~/.ssh/config (as shown above).
4. Authenticate both accounts with gh:
gh auth login # First account
gh auth login # Second account (select "Login with a different account")
5. Install direnv and add the shell hook:
brew install direnv
# Add to ~/.zshrc (or ~/.bashrc):
eval "$(direnv hook zsh)"
6. Create .envrc files at the appropriate directory levels:
# Default account at workspace root
echo 'export GH_TOKEN="$(gh auth token --user personal-username)"' > ~/code/.envrc
echo 'export GH_ACCOUNT="personal-username"' >> ~/code/.envrc
# Override for work directory
echo 'export GH_TOKEN="$(gh auth token --user work-username)"' > ~/code/work/.envrc
echo 'export GH_ACCOUNT="work-username"' >> ~/code/work/.envrc
7. Allow the .envrc files:
direnv allow ~/code/.envrc
direnv allow ~/code/work/.envrc
8. Clone work repos using the SSH alias:
git clone git@github-work:org/repo.git
Why Not includeIf?
Git's conditional includes (includeIf) are the commonly recommended approach. You add something like this to ~/.gitconfig:
[includeIf "gitdir:~/code/work/"]
path = ~/.gitconfig-work
This works well for git-specific config (user.name, user.email, signing key). But it has limitations:
- It only affects git config values — not shell environment variables
- The
ghCLI ignores it entirely - GitHub API clients, CI scripts, and other tooling that read
GH_TOKENaren't covered - It doesn't compose well when you need the same directory to affect multiple tools
direnv solves this at a lower level. By setting environment variables, it works with any tool that reads from the environment — which is most of them.
That said, you can combine both approaches. Use direnv for GH_TOKEN and includeIf for git author identity if you want per-directory commit emails without setting them per-repo.
Gotchas
A few things I ran into:
- direnv needs explicit allow: Every time you create or edit an
.envrc, you must rundirenv allow. This is a security feature — it prevents untrusted repos from injecting env vars into your shell. - Token refresh:
gh auth tokenreturns the currently stored token. If a token expires or gets revoked, you'll need to re-authenticate withgh auth loginfor that account. - Existing clones: If you have repos already cloned with
[email protected]:...URLs that should use a different account, update the remote:git remote set-url origin git@github-work:org/repo.git. - SSH agent confusion: If you use an SSH agent with multiple keys loaded, the agent may offer keys in an unpredictable order.
IdentitiesOnly yesin the SSH config prevents this by forcing SSH to use only the specified key.
This is one of those setups that takes 15 minutes to configure and then you never think about it again. The right account is always active, in every tool, based purely on where you are in the filesystem.
Consider leaving a small donation to support the blog.
Comments