Skip to content

Instantly share code, notes, and snippets.

@Drizzt321
Last active February 27, 2026 23:38
Show Gist options
  • Select an option

  • Save Drizzt321/47cf383a19535940a26616d982783d2e to your computer and use it in GitHub Desktop.

Select an option

Save Drizzt321/47cf383a19535940a26616d982783d2e to your computer and use it in GitHub Desktop.
Describing how to setup an automatic switching of Git identities (user.name, user.email) based on the directory you are in, as well as for switching authentication via github-cli (`gh`) in a similar manner.

Multi-Account Git + GitHub CLI Setup

A pattern for maintaining two separate GitHub identities on one machine (e.g. personal and work), with automatic switching based on which directory tree you are working in. No manual switching required after initial setup.


The Problem

Git and the gh CLI each have their own concept of "who you are":

  • Git embeds a name and email into every commit you make.
  • gh uses a stored OAuth token to make API calls (creating PRs, viewing issues, etc.).
  • Git push/pull also needs credentials — if you use gh as your git credential helper (the default when you run gh auth login), it reads the same stored token.

When you have two GitHub accounts on one machine, you need all three to automatically use the right identity depending on which repo you are in, without you having to remember to switch anything manually.


The Solution

Three tools work together, each handling one layer:

Layer Tool Mechanism
Commit identity (name/email) git includeIf "gitdir:" in ~/.gitconfig
gh CLI auth + git push/pull auth gh + direnv GH_CONFIG_DIR env var
Automatic env switching on cd direnv ~/work/.envrc

Why these compose cleanly

Git's includeIf is evaluated at parse time using the location of the repo's .git directory — it requires no external tools and has no runtime cost. It handles identity completely independently of everything else.

gh auth git-credential (the credential helper git calls during push/pull) is a normal gh subprocess. It inherits the full shell environment from whatever process invoked git, including GH_CONFIG_DIR. The gh library reads GH_CONFIG_DIR before anything else during startup, giving it absolute priority over the default config path. This means a single env var controls both gh CLI operations and git authentication simultaneously.

direnv sits at the shell level: it hooks into your shell's prompt command and loads or unloads .envrc files as your working directory changes. This means GH_CONFIG_DIR is always set correctly for the directory you are in, regardless of what you do next — run git push, run gh pr create, or open a subshell.


Prerequisites

  • git — any reasonably recent version
  • gh CLI — must be configured as your git credential helper. Verify with:
    git config --list | grep 'credential\..*helper='
    # expected output (same on all platforms when gh is configured):
    # credential.https://git.521000.best.helper=
    # credential.https://git.521000.best.helper=!/usr/bin/gh auth git-credential
    If it is not set, run gh auth login and it will be configured automatically.
  • direnv (github.com/direnv/direnv) — install via your package manager:
    # Debian/Ubuntu
    sudo apt install direnv
    
    # macOS
    brew install direnv
    
    # Fedora
    sudo dnf install direnv
    direnv has a wiki with recipes and integrations covering a wide range of tools — worth browsing if you use direnv beyond this pattern. Notable pages include integrations with Python (automatic virtualenv activation per project), Node (per-directory Node version management), VSCode (making the editor pick up direnv-managed environment variables), and Claude Code. These are just a few examples — the wiki also covers editors like Vim and Emacs, cloud tools like Terraform, secret managers like 1Password, and more, so if you use a tool not listed here it is worth checking whether a page exists for it.

Setup

Step 1: Hook direnv into your shell

Add to the end of your shell RC file (~/.bashrc, ~/.zshrc, etc.):

eval "$(direnv hook bash)"

Restart your shell or source ~/.bashrc.

This is a one-time operation. direnv will now silently watch for .envrc files as you navigate directories.

The above is for bash. Other shells use different hook syntax — see the direnv hook documentation for the correct snippet for your shell.

Note: IDEs and graphical editors launched outside your terminal may not inherit GH_CONFIG_DIR set by direnv. See IDEs and graphical editors in Edge Cases.

Step 2: Set up the gh config directories

Your personal account lives in the default ~/.config/gh/ (created when you first ran gh auth login). Create a separate isolated directory for your work account and authenticate into it:

GH_CONFIG_DIR=~/.config/gh-work gh auth login

Follow the interactive prompts as normal. This stores your work account's OAuth token in ~/.config/gh-work/hosts.yml — completely separate from your personal account's ~/.config/gh/hosts.yml.

Verify both accounts are set up independently:

gh auth status                                        # personal
GH_CONFIG_DIR=~/.config/gh-work gh auth status       # work

Lock down the work config directory and its contents:

chmod 700 ~/.config/gh-work
find ~/.config/gh-work -maxdepth 1 -type f -exec chmod 600 {} \;

These two commands together prevent any other user on the machine from looking into the directory or reading the files inside it.

Step 3: Create the work directory .envrc

Append the line to ~/work/.envrc (creates the file if it does not already exist):

echo 'export GH_CONFIG_DIR=$HOME/.config/gh-work' >> ~/work/.envrc

Then trust the file:

direnv allow ~/work

direnv allow is a one-time trust step. direnv will not execute any .envrc it has not been explicitly told to trust — this prevents a rogue .envrc from executing if one appeared somewhere in your filesystem.

After this, any shell session inside ~/work/ or any subdirectory of it will have GH_CONFIG_DIR set automatically. Moving out of that tree unsets it, reverting to the default ~/.config/gh/ (your personal account).

Step 4: Configure git identity

Add to ~/.gitconfig (the personal identity should already be there):

[user]
    name  = YourPersonalName
    email = personal@example.com

[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work

Create ~/.gitconfig-work:

[user]
    name  = YourWorkName
    email = work@yourcompany.com

The includeIf "gitdir:~/work/" directive merges the work config on top of the global config for any repo whose .git directory is under ~/work/. The [user] block in ~/.gitconfig-work overrides the global one.

Note: The trailing slash on ~/work/ is required for subtree matching, and applies to every includeIf block you add. Without it, git matches only a repo whose .git is at exactly that path — not any repo inside it. If you later add a third or fourth identity (e.g. ~/client-a/, ~/client-b/), the same rule applies to each:

[includeIf "gitdir:~/work/"]       # ✓ matches all repos under ~/work/
    path = ~/.gitconfig-work

[includeIf "gitdir:~/work"]        # ✗ matches only ~/work/.git itself
    path = ~/.gitconfig-work

Each additional identity needs its own includeIf block with a trailing slash, its own ~/.gitconfig-<name> file, and (if using Pattern B) its own named config directory and .envrc.

Note: If you use commit signing (GPG or SSH), the signingkey field under [user] in ~/.gitconfig applies to all repos unless overridden. Add a signingkey entry under [user] in ~/.gitconfig-work as well, so work commits are signed with the right key.

Alternative: match by remote URL instead of directory

Git 2.36+ supports includeIf "hasconfig:remote.*.url:<pattern>", which matches based on the repo's remote URL rather than its location on disk:

[includeIf "hasconfig:remote.*.url:git@github.com:mycompany/*"]
    path = ~/.gitconfig-work

[includeIf "hasconfig:remote.*.url:https://github.com/mycompany/*"]
    path = ~/.gitconfig-work

Both HTTPS and SSH URL forms need their own entry if you use both. The * in remote.*.url is a glob matching any remote name (origin, upstream, etc.).

This ties commit identity to something intrinsic to the repo — where it lives on GitHub — rather than where you cloned it on disk. A work repo cloned anywhere on your machine automatically gets the work identity. You can mix and match: use URL-based matching for git identity and still use direnv + GH_CONFIG_DIR for gh auth, since those are independent layers. Or combine both includeIf forms if you want belt-and-suspenders coverage.

The trade-off: URL-based matching only covers the git identity layer. It has no effect on GH_CONFIG_DIR — the direnv setup from Steps 1–3 is still required for gh CLI operations and git push authentication regardless of which includeIf form you use.

Step 5: Clear any existing cached credentials

If you have previously pushed to GitHub from this machine, a credential helper may have cached a token for github.com. Git tries credential helpers in order and uses the first one that returns a result — so a cached credential from an old helper will silently take priority over gh auth git-credential, regardless of which GH_CONFIG_DIR is active.

First, check which helpers are configured:

git config --list --show-origin --show-scope | grep 'credential\..*helper='

The pattern matches both the bare global form (credential.helper) and the URL-scoped form (credential.https://git.521000.best.helper) that gh auth login sets. A clean setup — only gh configured — looks like this on all platforms:

global  file:~/.gitconfig    credential.https://git.521000.best.helper=
global  file:~/.gitconfig    credential.https://git.521000.best.helper=!/usr/bin/gh auth git-credential
global  file:~/.gitconfig    credential.https://gist.521000.best.helper=
global  file:~/.gitconfig    credential.https://gist.521000.best.helper=!/usr/bin/gh auth git-credential

The empty-value entries are reset entries that clear any previously inherited helper — they are normal. If you see an additional non-empty helper= line beyond the gh entries, you need to clear it. What this looks like varies by platform:

  • macOS: credential.helper=osxkeychain
  • Linux: credential.helper=libsecret or credential.helper=/usr/lib/git-core/git-credential-libsecret
  • Windows 11: credential.helper=manager
  • WSL2: credential.helper=/mnt/c/Program Files/Git/mingw64/bin/git-credential-manager.exe or credential.helper=manager
  • Any platform (store): credential.helper=store

Use the platform-specific instructions below to clear the extra helper before continuing.

Enterprise note: All commands below use github.com as the hostname. If you are using GitHub Enterprise Server or GitHub Enterprise Cloud with Enterprise Managed Users, replace github.com with your enterprise hostname (e.g. github.mycompany.com or mycompany.ghe.com) — credential helpers key stored credentials by hostname, so the hostname must match exactly. This substitution is confirmed to work for GHES and GHEC EMU; for other enterprise configurations it should behave the same way, but we cannot guarantee it.

Try this first (works on any platform)

printf "host=github.com\nprotocol=https\n" | git credential reject

This tells the active helper chain to erase the stored credential for github.com. Note that the gh auth git-credential helper intentionally treats erase as a no-op — it does not revoke or delete anything. This command only affects legacy helpers (osxkeychain, libsecret, GCM, store, etc.) that are also present in the chain. If the grep above showed only gh entries, this command will silently do nothing, which means you are already clean.

Run it, then skip to Verify below. If credentials persist, use the platform-specific instructions.

macOS — Keychain

printf "host=github.com\nprotocol=https\n" | git credential-osxkeychain erase

Or open Keychain Access, search for github.com, and delete any entries associated with your git username.

Linux — GNOME Keyring

printf "host=github.com\nprotocol=https\n" | git credential-libsecret erase

Or open Passwords and Keys (Seahorse), find the github.com entry under Passwords, and delete it.

Linux — KDE KWallet

Open KWallet Manager (kwalletmanager5), navigate to the Passwords folder, find any entry for github.com, and delete it. If you have git-credential-kwallet installed:

printf "host=github.com\nprotocol=https\n" | git credential-kwallet erase

Windows 11 — Git Credential Manager

Open Credential Manager (Start menu → search "Credential Manager"), select Windows Credentials, find the entry named git:https://github.com, and click Remove. Or from the command line:

printf "host=github.com\nprotocol=https\n" | git credential-manager erase

GCM internally normalises gist.github.com to github.com, so a single erase of https://github.com covers both hosts.

Windows 11 WSL2

WSL2 is commonly configured to forward credentials to the Windows Git Credential Manager. Check which helper is in use:

git config --list --global | grep 'credential\..*helper='
# common output — GCM via full path:
# credential.helper=/mnt/c/Program Files/Git/mingw64/bin/git-credential-manager.exe
# or via name if GCM is in PATH:
# credential.helper=manager

If WSL2 is using the Windows GCM bridge, the credential is stored on the Windows side — clear it via Windows Credential Manager as described above. If WSL2 has its own credential helper configured independently, use the appropriate Linux instructions instead.

git-credential-store (any platform)

If credential.helper = store appears in your config, credentials are stored in plaintext in ~/.git-credentials. Remove the github.com line:

[ -f ~/.git-credentials ] && \
  grep -v 'github\.com' ~/.git-credentials > ~/.git-credentials.tmp && \
  mv ~/.git-credentials.tmp ~/.git-credentials

git-credential-cache (any platform)

A daemon-based in-memory cache that expires automatically after a configurable timeout (default 15 minutes) and also clears on reboot. To clear immediately:

git credential-cache exit

Verify

Confirm the correct git identity is active from inside any repo under ~/work/:

git config --show-origin --show-scope user.name
git config --show-origin --show-scope user.email
# expected: global  file:~/.gitconfig-work    work@yourcompany.com

Note: includeIf only fires inside a git repository — running this from ~/work/ itself (not inside a repo) will show the global value, not the work override.

Confirm the correct gh account is active with:

gh auth status

Outside ~/work/ this should show your personal account; inside ~/work/ (with GH_CONFIG_DIR set by direnv) it should show your work account.

If you want to verify the full credential pipeline end-to-end:

printf "host=github.com\nprotocol=https\n" | git credential fill

Note: This prints password=<live token> to stdout. It confirms that gh auth git-credential is being invoked and respects GH_CONFIG_DIR, but it exposes a live credential in your terminal. Avoid running it in sessions with terminal logging or recording enabled, and clear your terminal afterwards.


How a git push works end-to-end (inside ~/work/)

  1. Your shell has GH_CONFIG_DIR=~/.config/gh-work (set by direnv).
  2. git push needs credentials. It forks gh auth git-credential get.
  3. The forked subprocess inherits the environment, including GH_CONFIG_DIR.
  4. gh resolves its config directory: GH_CONFIG_DIR is set, so it reads ~/.config/gh-work/hosts.yml.
  5. It returns the work account's OAuth token to git.
  6. The push authenticates as your work account.

The commit itself already has the right author because includeIf caused ~/.gitconfig-work to be loaded when git read the repo's config.


How a gh pr create works end-to-end (inside ~/work/)

  1. Your shell has GH_CONFIG_DIR=~/.config/gh-work.
  2. gh pr create reads its config from ~/.config/gh-work/hosts.yml.
  3. The PR is created under your work account. No switching required.

Alternative Pattern: No Default Account (Fail-Loud)

The setup above uses ~/.config/gh/ as the personal account fallback. This means any directory without an active .envrc — including one accidentally shadowed by an intermediate .envrc missing source_up_if_exists / source_up — silently uses your personal account. For most people this is acceptable: the failure mode is a push or PR landing on the wrong account, which is visible and correctable.

If you would prefer that any missing or broken .envrc causes an immediate, obvious auth failure rather than silently using the wrong account, use this alternative instead:

Do not authenticate into ~/.config/gh/ at all. Leave it absent or empty. Create two named config directories and two .envrc files:

# Set up both accounts in named directories
GH_CONFIG_DIR=~/.config/gh-personal gh auth login
GH_CONFIG_DIR=~/.config/gh-work     gh auth login

chmod 700 ~/.config/gh-personal ~/.config/gh-work
find ~/.config/gh-personal ~/.config/gh-work -maxdepth 1 -type f -exec chmod 600 {} \;

For each directory, append the line to the .envrc (creates the file if it does not already exist):

# ~/code/.envrc
echo 'export GH_CONFIG_DIR=$HOME/.config/gh-personal' >> ~/code/.envrc
direnv allow ~/code

# ~/work/.envrc
echo 'export GH_CONFIG_DIR=$HOME/.config/gh-work' >> ~/work/.envrc
direnv allow ~/work

Now, outside of ~/code/ and ~/work/, GH_CONFIG_DIR is unset and gh falls back to ~/.config/gh/ — which has no authenticated account. Any gh command or git push in that context fails immediately with an auth error, making the missing .envrc obvious rather than silent.

The same applies if an intermediate .envrc shadows a parent one without source_up: GH_CONFIG_DIR gets unset for that subtree, and operations fail loudly until you fix the .envrc.

Trade-off: You must remember to create an appropriate .envrc (or pass GH_CONFIG_DIR explicitly) anywhere outside ~/code/ and ~/work/ that you want to use gh or push to GitHub. This is slightly more friction but eliminates the silent-wrong-account failure mode entirely.

The git identity side (includeIf) can be made equally fail-loud. By default, if no user.name or user.email is configured, git attempts to auto-detect them from your system username and hostname and commits anyway — silently, with a potentially unrecognisable identity.

Two settings together prevent this entirely:

1. Remove any existing global identity, if you have one set:

git config --global --unset user.name
git config --global --unset user.email

2. Set user.useConfigOnly = true in ~/.gitconfig:

git config --global user.useConfigOnly true

3. Add includeIf blocks for both directories in ~/.gitconfig:

[includeIf "gitdir:~/code/"]
    path = ~/.gitconfig-personal

[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work

Create ~/.gitconfig-personal:

[user]
    name  = YourPersonalName
    email = personal@example.com

(~/.gitconfig-work was already created in Step 4.)

user.useConfigOnly tells git never to auto-detect identity — user.name and user.email must be explicitly configured at some level or git refuses to commit. Outside ~/code/ and ~/work/, no identity is configured and includeIf has no match, so any commit attempt fails immediately with a clear error. Inside those directories, includeIf provides the explicit identity and commits work normally.


Edge Cases and Caveats

Token storage: hosts.yml vs OS keyring

gh auth login may store your token in the OS keyring rather than directly in hosts.yml, depending on your platform and gh version. GH_CONFIG_DIR still works in this case: gh reads the active username from hosts.yml (which is in GH_CONFIG_DIR), then uses that username to look up the token in the keyring by service name. So the right token is still retrieved, just via an extra indirection. You do not need to do anything differently.

Caution

Forcing file-based token storage is not recommended. Choosing "store in plain text" bypasses your OS keyring's at-rest encryption — the token is written as readable text into hosts.yml, accessible to any process running as your user, and included verbatim in any unencrypted backup. The OS keyring exists specifically to prevent this. Only do this if you have a concrete portability requirement and understand what you are giving up.

If despite the above you need to force file-based storage, you can do so during login:

GH_CONFIG_DIR=~/.config/gh-work gh auth login --git-protocol https
# when asked about credential storage, choose "store in plain text"

--git-protocol https tells gh to configure the git credential helper for HTTPS during login. Without it, gh may default to SSH, which bypasses the credential helper entirely — the helper is only invoked for HTTPS push/pull operations. It has no effect on how the token itself is stored; the plaintext storage choice is made separately via the interactive prompt.

HTTPS vs SSH remotes

This pattern handles HTTPS remotes (token-based auth via the credential helper). If a work repo is using an SSH remote, gh auth git-credential is never invoked — SSH handles authentication instead, using whichever key matches the github.com entry in ~/.ssh/config. In that case, GH_CONFIG_DIR has no effect on the push/pull auth (though gh CLI operations still use it). If you have two GitHub accounts on the same host and use SSH, you need separate SSH keys and host aliases — that is a separate setup not covered here.

GH_TOKEN takes precedence over GH_CONFIG_DIR

If GH_TOKEN (or GITHUB_TOKEN) is set in the environment, gh uses it directly and ignores whatever is in GH_CONFIG_DIR/hosts.yml. If both are set, GH_TOKEN takes priority over GITHUB_TOKEN.

This is intentional behaviour in CI environments — GitHub Actions automatically injects GITHUB_TOKEN into every workflow run so that gh and git authenticate as the Actions bot for that repo, without any manual setup. In that context the override is exactly what you want.

The hazard in an interactive session is accidental leakage: a CI bootstrap script sourced into your shell, a tool that sets GH_TOKEN globally, or a .envrc that exports it — any of these will silently override GH_CONFIG_DIR for as long as the variable is set. If authentication seems wrong despite the setup looking correct, check whether the variable is present:

echo $GH_TOKEN
echo $GITHUB_TOKEN

If either is set unexpectedly, find the source and remove it from your interactive shell environment.

Repos cloned outside their expected tree

includeIf "gitdir:~/work/" and direnv .envrc files both trigger on physical filesystem location. A work repo cloned into ~/code/ or anywhere outside ~/work/ will use your personal identity and personal gh auth (Pattern A), or fail with an auth error (Pattern B).

The fix is simply to keep repos in the right directory tree. If you need a repo in an unexpected location, set the identity manually in that repo:

git config user.email work@yourcompany.com
git config user.name  YourWorkName

And either add a local .envrc pointing to the right config dir, or pass GH_CONFIG_DIR explicitly when running gh commands there.

Git worktrees checked out outside their repo's directory tree

git worktree add lets you check out a branch from an existing repo into a separate directory. The linked worktree contains a .git file (not a directory) that points back to the original repo's .git directory.

This creates an asymmetry between the two layers of this pattern:

  • includeIf "gitdir:~/work/" follows the pointer and evaluates against the real .git location — which is still inside ~/work/. So the work git identity is applied correctly, even in a worktree checked out elsewhere.
  • direnv only cares about where your shell currently is. A worktree at ~/tmp/hotfix/ is outside ~/work/, so GH_CONFIG_DIR is not set. In Pattern A, gh silently uses your personal account; in Pattern B, it fails with an auth error.

The result: commits get the right work identity, but gh operations and git push authenticate as the wrong account (or not at all), with no warning.

This only affects worktrees checked out outside their repo's directory tree, which is uncommon. The straightforward fix is to keep linked worktrees inside ~/work/ alongside their main repo. If you need one elsewhere, add a local .envrc in the worktree directory:

echo 'export GH_CONFIG_DIR=$HOME/.config/gh-work' > ~/tmp/hotfix/.envrc
direnv allow ~/tmp/hotfix

Unusual environment or tool configurations

Some tools — IDEs, wrapper scripts, git GUIs — set the GIT_DIR environment variable explicitly to point git at a specific repository. If GIT_DIR is set to a path outside ~/work/, includeIf "gitdir:~/work/" will not match, and the work identity will not be applied, even if you are working in what you consider a work repo.

This is technically correct behaviour — git is doing exactly what it was told — but it can produce unexpected results if a tool is misdirecting GIT_DIR without you realising. If commits are getting the wrong identity, check whether GIT_DIR is set:

echo $GIT_DIR

If it is set to a path outside ~/work/, the source is whatever tool or script set it. The fix is at that layer, not in the git config.

Symlinks

includeIf "gitdir:~/work/" expands ~ but does not resolve symlinks in the pattern itself. Git matches against the real (symlink-resolved) path of the .git directory. If ~/work is itself a symlink to another location, the pattern ~/work/ will not match — the pattern stays as the symlink path while the .git path resolves to the real location, so they diverge.

The fix is to use the real path in the pattern:

realpath ~/work   # e.g. /opt/projects/work
[includeIf "gitdir:/opt/projects/work/"]
    path = ~/.gitconfig-work

On macOS, where APFS volumes are case-insensitive by default, you can use gitdir/i: for case-insensitive path matching — uppercase and lowercase in the path are treated as equivalent:

[includeIf "gitdir/i:~/work/"]
    path = ~/.gitconfig-work

This is a separate concern from symlink resolution and does not affect it.

Test with git config user.email inside a work repo to confirm the right identity is active.

New terminal windows opened inside ~/work/

direnv hooks into the shell prompt, not just cd. When a new terminal opens directly into a directory inside ~/work/, direnv fires on the first prompt and loads the .envrc. This works correctly — you do not need to cd out and back in.

Subshells and scripts

direnv sets env vars in the interactive shell. A subshell (e.g. bash -c 'git push') inherits exported env vars, so GH_CONFIG_DIR is present. However, scripts invoked via cron, systemd, or other non-interactive mechanisms do not go through your shell's prompt hook and will not have GH_CONFIG_DIR set. For automated scripts, pass the variable explicitly:

GH_CONFIG_DIR=~/.config/gh-work gh pr list

IDEs and graphical editors

direnv hooks into the shell prompt — it only runs when an interactive shell session changes directory. IDEs and graphical editors launched from a desktop icon, dock, or application menu start their own process outside your shell, so they do not go through the direnv hook and will not have GH_CONFIG_DIR set.

This means git identity (name/email) still works correctly — includeIf is evaluated by git itself against the filesystem every time git runs, with no shell involvement. But gh CLI operations and git push authentication from inside the IDE will use the wrong account (Pattern A) or fail with an auth error (Pattern B), silently.

The simplest fix is to open the IDE from a direnv-activated terminal:

cd ~/work/myrepo
code .        # or idea ., or whatever your editor's CLI is

The IDE process inherits the full shell environment, including GH_CONFIG_DIR.

For a more robust solution that works regardless of how you open the IDE, check the direnv wiki — several editors have community plugins that load .envrc files directly into the IDE's environment when you open a project.

Adding new .envrc files in subdirectories

If you create a new .envrc inside a subdirectory of ~/work/ (e.g. ~/work/project/.envrc), direnv will use that file instead of ~/work/.envrc for that subtree — they do not merge. If the subdirectory .envrc does not re-export GH_CONFIG_DIR, it will be unset there. To inherit the parent while adding more variables, explicitly source the parent at the top of the subdirectory .envrc using source_up_if_exists:

# ~/work/project/.envrc
source_up_if_exists   # loads nearest parent .envrc if one exists
export SOME_OTHER_VAR=value

source_up_if_exists silently does nothing if no parent .envrc is found. If you want the .envrc to fail loudly when there is no parent — for example, to catch a misconfigured directory layout — use source_up instead, which errors out if no parent .envrc exists.

direnv allow after editing .envrc

Whenever you edit ~/work/.envrc, direnv will block it and require another direnv allow ~/work. This is intentional — it prevents an edited file from executing without your explicit sign-off. You will see a warning in your prompt when this happens.

Verifying the active identity at any time

# Which git identity will be used here?
# Must be run from inside a git repository — includeIf does not fire outside one.
git config --show-origin --show-scope user.name
git config --show-origin --show-scope user.email

# Which gh account is active here?
gh auth status

If you are not yet inside a repo, create a temporary one to test:

git init ~/work/tmp-identity-test
cd ~/work/tmp-identity-test
git config --show-origin --show-scope user.email
# expected: global  file:~/.gitconfig-work    work@yourcompany.com
rm -rf ~/work/tmp-identity-test

Both commands are cheap and safe to run any time you are unsure.


Summary of Files Touched

Pattern A (personal as default fallback)

File Purpose
~/.bashrc (or shell RC) eval "$(direnv hook bash)"
~/.config/gh/ Personal gh config (existing default)
~/.config/gh-work/ Work gh config directory
~/work/.envrc Sets GH_CONFIG_DIR=~/.config/gh-work
~/.gitconfig Personal identity (default) + includeIf for work
~/.gitconfig-work Work identity (name/email override)

Pattern B (no default — fail-loud)

File Purpose
~/.bashrc (or shell RC) eval "$(direnv hook bash)"
~/.config/gh-personal/ Personal gh config directory
~/.config/gh-work/ Work gh config directory
~/code/.envrc Sets GH_CONFIG_DIR=~/.config/gh-personal
~/work/.envrc Sets GH_CONFIG_DIR=~/.config/gh-work
~/.gitconfig No default identity; user.useConfigOnly = true; includeIf for both directories
~/.gitconfig-personal Personal identity (name/email)
~/.gitconfig-work Work identity (name/email)

Other Tools That Support This Pattern

Note: I have not personally tested the tool-specific instructions in this section. The git + gh setup described in the rest of this document is tested and validated; this section was assembled from documentation and community sources and represents a best-effort reference. The environment variables and general patterns described here are accurate to the linked documentation, but there may be edge cases, version-specific behaviour, or subtle interactions that only surface in practice. If you find something that does not work as described, I would appreciate a comment or correction.

The combination of environment variables and direnv is not unique to gh. Many developer tools follow the same general approach: an environment variable controls which configuration or credentials the tool uses, and direnv automatically sets that variable based on your working directory. If you already have direnv hooked into your shell and a directory-based project layout, extending this pattern to other tools is usually a one-line addition to your .envrc.

Two sub-patterns

Tools generally fall into one of two categories:

Config directory redirection — an environment variable points the tool at an entirely separate configuration directory. This is what GH_CONFIG_DIR does. The tool reads all of its state — credentials, preferences, cached data — from the alternate directory. This provides full isolation between identities: each directory is a self-contained world, and there is no risk of settings from one identity leaking into another. The trade-off is that you maintain multiple copies of the full configuration, and any non-identity settings (output format preferences, default regions, plugin config) are also separated. This is usually desirable — your work and personal accounts likely should have different default regions or output formats — but it means changes to shared preferences must be applied to each directory independently.

Profile selection — an environment variable selects a named profile within a single configuration file. The tool still reads from its default config location, but uses a different section of it. This is simpler to set up and avoids duplicating shared settings, but does not provide full isolation: all profiles share the same file, and a misconfigured profile name silently falls back to the default rather than failing. It also means credentials for all identities live in the same file, which may matter for your threat model.

Both sub-patterns work with direnv. Choose based on how much isolation you need and how many non-identity settings differ between your contexts.

Tools and their environment variables

The following is not exhaustive, but covers tools that developers commonly need to use with multiple identities or environments. For each tool, the relevant environment variables are listed along with which sub-pattern they follow, any security considerations, and links to documentation.

AWS CLI

Variable Sub-pattern What it controls
AWS_PROFILE Profile selection Selects a named profile from ~/.aws/config and ~/.aws/credentials
AWS_CONFIG_FILE Config directory redirection Path to an alternate config file
AWS_SHARED_CREDENTIALS_FILE Config directory redirection Path to an alternate credentials file

AWS_PROFILE is the simplest approach: define profiles in ~/.aws/config and ~/.aws/credentials using aws configure --profile <name>, then set AWS_PROFILE in your .envrc:

export AWS_PROFILE=work

For full isolation, redirect both files:

export AWS_CONFIG_FILE=$HOME/.aws/config-work
export AWS_SHARED_CREDENTIALS_FILE=$HOME/.aws/credentials-work

AWS credentials files contain long-lived access keys in plaintext by default. Lock down file permissions (chmod 600) and prefer short-lived credentials via aws sso login or role assumption where possible.

This is a mature, well-documented pattern — the AWS CLI has supported these variables for years and essentially every AWS SDK and tool respects them.

Docs: AWS CLI environment variables, Configuration and credential files

Google Cloud CLI (gcloud)

Variable Sub-pattern What it controls
CLOUDSDK_ACTIVE_CONFIG_NAME Profile selection Selects a named configuration
CLOUDSDK_CONFIG Config directory redirection Path to the entire config directory (default ~/.config/gcloud)

gcloud has a built-in concept of named configurations (gcloud config configurations create <name>). The easiest approach is to create configurations for each identity and switch between them:

export CLOUDSDK_ACTIVE_CONFIG_NAME=work

For full isolation — including separate OAuth tokens and cached data — redirect the entire config directory:

export CLOUDSDK_CONFIG=$HOME/.config/gcloud-work

Individual properties can also be overridden via CLOUDSDK_<SECTION>_<PROPERTY> (e.g. CLOUDSDK_CORE_PROJECT, CLOUDSDK_COMPUTE_REGION), which is useful for setting project-level defaults without creating a full separate configuration.

Google's own documentation suggests using direnv for this exact purpose. This is a mature pattern.

Docs: Managing gcloud CLI configurations

Azure CLI (az)

Variable Sub-pattern What it controls
AZURE_CONFIG_DIR Config directory redirection Path to the entire config directory (default ~/.azure)

Azure CLI does not have a built-in named-profile mechanism like AWS or gcloud. The only way to maintain multiple authenticated sessions is to redirect the entire config directory:

export AZURE_CONFIG_DIR=$HOME/.azure-work

Then run az login with that variable set to authenticate the work identity into the alternate directory.

Individual settings can be overridden via AZURE_<SECTION>_<NAME> (e.g. AZURE_CORE_OUTPUT, AZURE_DEFAULTS_LOCATION), but there is no AZURE_DEFAULTS_SUBSCRIPTION — the active subscription is stored in the config directory's profile state, not as a standalone override. This is a known gap.

The config directory contains cached OAuth tokens. Lock down permissions on alternate directories the same way you would for ~/.azure.

This is a mature pattern, well-supported by the CLI.

Docs: Azure CLI configuration

Kubernetes (kubectl)

Variable Sub-pattern What it controls
KUBECONFIG Config directory redirection Path to one or more kubeconfig files

KUBECONFIG can point to a single file or a colon-separated list of files that kubectl merges in memory:

export KUBECONFIG=$HOME/.kube/config-work

Or merge multiple files:

export KUBECONFIG=$HOME/.kube/config-work:$HOME/.kube/config-shared

kubectl also supports --context for per-command context selection without changing the active context, and tools like kubectx make interactive switching more ergonomic.

Kubeconfig files contain cluster certificates and authentication tokens. The same file-permission caution applies. Note that unlike most tools listed here, KUBECONFIG supports merging multiple files — this is a feature unique to kubectl and extremely useful for keeping per-cluster configs separate while still being able to use them together.

This is the most mature pattern on this list — multi-cluster kubeconfig management is a core part of the Kubernetes workflow.

Docs: Organizing cluster access, Configure access to multiple clusters

Terraform

Variable Sub-pattern What it controls
TF_CLI_CONFIG_FILE Config directory redirection Path to the CLI configuration file (credentials, plugin cache, etc.)
TF_WORKSPACE Profile selection Selects a named workspace
TF_DATA_DIR Config directory redirection Path to the .terraform data directory

For multi-environment Terraform work, the most common direnv pattern is setting TF_WORKSPACE and cloud-provider credentials together:

export TF_WORKSPACE=staging
export AWS_PROFILE=staging

TF_CLI_CONFIG_FILE is more relevant when you need separate Terraform Cloud or Enterprise credentials (stored in the CLI config's credentials block) for different organizations:

export TF_CLI_CONFIG_FILE=$HOME/.terraformrc-work

Terraform's own documentation warns that TF_WORKSPACE is recommended only for non-interactive usage, since it is easy to forget the variable is set in a local shell — exactly the kind of hazard that direnv mitigates by automatically unsetting it when you leave the directory.

The CLI config file (~/.terraformrc or the path in TF_CLI_CONFIG_FILE) can contain API tokens for Terraform Cloud. Protect it accordingly.

This is a mature pattern, widely used in CI/CD.

Docs: Terraform CLI environment variables, CLI configuration file

Docker

Variable Sub-pattern What it controls
DOCKER_CONFIG Config directory redirection Path to the Docker config directory (default ~/.docker)

Docker stores registry credentials in $DOCKER_CONFIG/config.json. To maintain separate registry credentials for different contexts:

export DOCKER_CONFIG=$HOME/.docker-work

Then docker login within that context writes to the alternate config.

This is particularly useful when you need to authenticate to the same registry (e.g. Docker Hub, a company Artifactory) with different accounts — Docker's auth model is one-credential-per-registry within a single config file, so multiple accounts on the same registry require separate config directories.

By default, Docker stores credentials in plaintext in config.json. Use a credential helper (docker-credential-osxkeychain, docker-credential-secretservice, etc.) to store tokens in your OS keyring instead. When using DOCKER_CONFIG with credential helpers, ensure the helper is configured in each alternate config.

This is a mature pattern.

Docs: Docker CLI configuration

Claude Code

Variable Sub-pattern What it controls
CLAUDE_CONFIG_DIR Config directory redirection Path to the Claude Code config directory (default ~/.claude)
ANTHROPIC_API_KEY Direct credential injection API key used for authentication

For maintaining separate Claude Code identities (e.g. personal subscription vs work API key):

export CLAUDE_CONFIG_DIR=$HOME/.claude-work

Or inject a specific API key directly:

export ANTHROPIC_API_KEY=sk-ant-...

Maturity caveat: CLAUDE_CONFIG_DIR is not yet fully documented in Anthropic's official documentation, and some features do not fully respect it — Claude Code may still create local .claude/ directories in workspace roots regardless of this variable, and IDE integrations (e.g. the VS Code extension) may look in the default location rather than the one specified by the variable. The core CLI respects it for credentials and project state, but expect rough edges. This is improving with each release.

ANTHROPIC_API_KEY is a plaintext API key. Keep it out of .envrc files that might be committed to version control — use direnv's dotenv or source_env with a gitignored file instead.

Docs: Claude Code settings, direnv wiki — Claude Code

npm

Variable Sub-pattern What it controls
NPM_CONFIG_USERCONFIG Config directory redirection Path to the user-level .npmrc file (default ~/.npmrc)

To use a different .npmrc (containing different registry URLs, auth tokens, or scope configurations):

export NPM_CONFIG_USERCONFIG=$HOME/.npmrc-work

Any npm configuration option can also be set directly via environment variables using the npm_config_<option> naming convention (e.g. npm_config_registry, npm_config_//registry.npmjs.org/:_authToken).

npm also supports per-project .npmrc files in the project root, which are loaded automatically without direnv. For registry auth tokens that differ by project, a per-project .npmrc may be simpler than direnv — but keep it gitignored if it contains tokens.

.npmrc files can contain registry authentication tokens in plaintext. npm requires these files to have mode 0600; files with broader permissions are silently ignored.

This is a mature pattern.

Docs: npm config, npmrc

SSH

SSH does not follow either sub-pattern — there is no SSH_CONFIG_DIR environment variable. SSH handles multiple identities natively through ~/.ssh/config using Host aliases, IdentityFile, and IdentitiesOnly directives. If you use SSH remotes instead of HTTPS, GH_CONFIG_DIR has no effect on push/pull authentication — key selection is handled entirely by SSH's own config, independent of direnv. See HTTPS vs SSH remotes above for details.

Docs: man ssh_config

Composing multiple tools in a single .envrc

Because direnv manages all environment variables for a directory tree, you can combine any of the above in a single .envrc. A realistic work directory might look like:

# ~/work/.envrc
export GH_CONFIG_DIR=$HOME/.config/gh-work
export AWS_PROFILE=work
export CLOUDSDK_ACTIVE_CONFIG_NAME=work
export KUBECONFIG=$HOME/.kube/config-work
export DOCKER_CONFIG=$HOME/.docker-work
export CLAUDE_CONFIG_DIR=$HOME/.claude-work
export TF_CLI_CONFIG_FILE=$HOME/.terraformrc-work

All of these activate when you cd into ~/work/ and deactivate when you leave. Combined with git's includeIf for commit identity, this gives you a single, declarative file that controls your entire toolchain identity.

If a subdirectory needs to override or extend any of these — for example, a project that deploys to a different AWS account — add a .envrc in that subdirectory with source_up_if_exists at the top to inherit the parent's variables before overriding the ones that differ. See Adding new .envrc files in subdirectories above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment