How to Use Work & Personal GitHub Accounts on Ubuntu (HTTPS + gh CLI)
Prerequisites
You'll need these on your Ubuntu machine before starting:
- Git —
sudo apt install -y git - GitHub CLI (
gh) — Install via the official guide: GitHub CLI → Installation
Step 1 — Per-directory git identity
Everything below keys off one value: the repo's git config user.name. Instead of setting it (and user.email) per repo by hand, let the directory decide using Git's includeIf.
This needs a folder layout where the path tells you who owns a repo. Group clones by their GitHub owner, the same way GitHub's URLs do:
~/Projects/github/
├── personal-username/ # your personal repos
├── your-org/ # work
└── another-client-org/ # work
~/Projects/github/<owner>/<repo> matches github.com/<owner>/<repo>, so you always know where a clone goes. Only your personal owner needs a rule; every work owner shares the default.
Set your default (work) identity in ~/.gitconfig:
[user]
name = work-username
email = work@example.com
[init]
defaultBranch = main
Now add a conditional include at the bottom of ~/.gitconfig that overrides the identity for anything under your personal owner's folder:
[includeIf "gitdir:~/Projects/github/personal-username/"]
path = ~/.config/git/personal-username.gitconfig
And create ~/.config/git/personal-username.gitconfig:
mkdir -p ~/.config/git
nano ~/.config/git/personal-username.gitconfig
Paste:
[user]
name = personal-username
email = personal@example.com
gitdir:~/Projects/github/personal-username/ matters: it means "any repo inside this directory". Every repo under it uses your personal name and email, and everything else falls back to work. Don't set user.name in a repo's own .git/config instead. A repo you forget to set falls back to work without telling you.The wrapper in Step 2 picks the gh account from this user.name, so it must equal your GitHub login exactly.
Step 2 — Make gh auto-select the right account per repo
gh has no built-in "use account X in directory Y" feature (cli/cli#326). The workaround, an idea from @uncenter in that issue, is a small wrapper script placed ahead of the real gh on your PATH. Before every gh call it reads the repo's git config user.name, looks up that account's token, and runs the real gh with it in GH_TOKEN.
Because it's a script on PATH (not a shell function), it works everywhere: interactive shells, scripts, and CI alike.
Create ~/.local/bin/gh:
mkdir -p ~/.local/bin
nano ~/.local/bin/gh
Paste:
#!/usr/bin/env bash
# gh -- runs the real gh as the account the repo's `git config user.name` names.
#
# gh has no per-directory account (https://github.com/cli/cli/issues/326), so
# this sits ahead of the real gh on PATH and hands it that login's token.
# The account follows the working directory, not a repo named with -R or a
# URL: run gh from inside the clone it acts on.
# The next gh on PATH that is not this script.
self="$(readlink -f "$0")"
REAL_GH=""
while IFS= read -r cand; do
[ "$(readlink -f "$cand")" = "$self" ] && continue
REAL_GH="$cand"
break
done < <(type -aP gh)
if [ -z "$REAL_GH" ]; then
echo "gh wrapper: could not find the real gh on PATH" >&2
exit 127
fi
# GH_TOKEN for this one process, not `gh auth switch`: the switch changes the
# active account for every gh on the machine, so parallel calls race.
# A GH_TOKEN already set wins, and `gh auth` commands other than
# git-credential refuse to run under GH_TOKEN, so they keep the keyring.
if [ -z "${GH_TOKEN:-}" ] && { [ "${1:-}" != auth ] || [ "${2:-}" = git-credential ]; }; then
want="$(git config user.name 2>/dev/null)"
if [ -n "$want" ] && token="$("$REAL_GH" auth token -u "$want" 2>/dev/null)"; then
export GH_TOKEN="$token"
fi
fi
exec "$REAL_GH" "$@"
Make it executable:
chmod +x ~/.local/bin/gh
Make sure the wrapper wins on PATH
The wrapper only works if ~/.local/bin comes before /usr/bin on your PATH. On Ubuntu, ~/.local/bin is usually added automatically, but make it explicit in ~/.bashrc:
# ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
Reload and confirm gh now resolves to the wrapper:
source ~/.bashrc
hash -r # clear any cached path to the old gh
command -v gh # -> /home/<you>/.local/bin/gh
How the wrapper works
- It reads
git config user.namefor the current repo, which yourincludeIfrule sets. gh auth token -u <name>prints that account's token from the keyring. If no login matches, it fails andghfalls back to the active account.- The token goes into
GH_TOKENfor this oneghprocess only.ghreadsGH_TOKENbefore the active account, and nothing else on the machine sees it. gh auth login,switchandstatusare skipped, because they refuse to run whileGH_TOKENis set.gh auth git-credentialstill gets the token, which Step 3 relies on.exec "$REAL_GH" "$@"replaces the process, so the exit code, stdin/stdout/stderr and signals pass straight through.- It runs the next
ghonPATHthat isn't itself, so it never calls itself in a loop and needs no hard-coded path to the real binary.
gh auth switch instead. That works until two gh commands for different accounts run at once, from scripts or coding agents. The switch is one setting shared by the whole machine, so the second command flips the account under the first. The first then fails with an error such as GraphQL: Could not resolve to a Repository with the name 'owner/repo'. GH_TOKEN gives each process its own account, so the commands can't affect each other.Step 3 — Route git over HTTPS through the wrapper
Over HTTPS, git asks a credential helper for the password. Make that helper the wrapper and git gets the folder's token too, so clone, pull and push pick the account the same way gh does. That's why this setup needs no SSH keys.
Add to ~/.gitconfig:
[credential "https://github.com"]
helper =
helper = !$HOME/.local/bin/gh auth git-credential
[credential "https://gist.github.com"]
helper =
helper = !$HOME/.local/bin/gh auth git-credential
The empty helper = resets the list, so a helper set earlier in your config isn't asked first.
Don't run gh auth setup-git. It rewrites these lines to the real gh's path, which skips the wrapper, and every push goes out as the active account. Check:
git config --get-all credential.https://github.com.helper
!$HOME/.local/bin/gh auth git-credential
The blank first line is the empty helper =.
Step 4 — Authenticate gh for both accounts
Log in to each account once over HTTPS, work first and then personal. gh stores them side by side.
gh auth login --web --git-protocol https # work account
gh auth login --web --git-protocol https # personal account
Before each run, sign in to github.com in your browser as that account. The web flow authorizes whoever the browser is signed in as.
gh asks "Authenticate Git with your GitHub credentials?" only when git's credential helper isn't gh. With Step 3 in place it skips the question and adds the workflow scope, which git needs to push changes under .github/workflows/. If it asks anyway, answer No, fix Step 3 and log in again. Yes runs gh auth setup-git, which points git at the real gh and skips the wrapper.
Confirm both are present:
gh auth status
github.com
✓ Logged in to github.com account personal-username (keyring)
- Active account: true
✓ Logged in to github.com account work-username (keyring)
- Active account: false
You never need gh auth switch: the wrapper picks the account on every call. The active account only matters when a folder's user.name matches no login.
Step 5 — Clone and verify
Clone with a plain HTTPS URL into the owner's folder:
git clone https://github.com/your-org/work-repo.git ~/Projects/github/your-org/work-repo
git clone https://github.com/personal-username/personal-repo.git ~/Projects/github/personal-username/personal-repo
While cloning, git already treats the new folder as the repo, so its includeIf identity picks the account. You can run both from anywhere.
gh repo clone owner/repo works too, but the wrapper picks the account from the folder you run it in, not the folder you clone into. From ~ it runs as work, so a private personal repo fails with Could not resolve to a Repository. Run it from inside one of that owner's clones, or use git clone.
Then check each repo:
# Work repo -> should report the work account
cd ~/Projects/github/your-org/work-repo
git config user.name # work-username
gh api user --jq '.login' # work-username
# Personal repo -> should report the personal account
cd ~/Projects/github/personal-username/personal-repo
git config user.name # personal-username
gh api user --jq '.login' # personal-username
# A private repo authenticates over HTTPS as the folder's account
git ls-remote origin HEAD
If each command prints what its comment says without you switching anything, you're done. Commit identity, gh and git over HTTPS all follow the directory now. gh auth status still shows the same active account as before, because the wrapper never changes it.
Already set up the SSH version of this guide?
- Add the helper lines from Step 3.
- Log in to both accounts again as in Step 4. That switches
ghto HTTPS and adds theworkflowscope. - In each clone, point
originat HTTPS:git remote set-url origin https://github.com/<owner>/<repo>.git
The SSH keys can go after that. Delete them from both accounts at github.com/settings/keys, then remove ~/.ssh/id_work*, ~/.ssh/id_personal* and the github-work / github-personal blocks in ~/.ssh/config.
Troubleshooting
- Git asked something other than the wrapper.
git config --get-all credential.https://github.com.helpermust end with the wrapper line from Step 3.gh auth setup-git, or answering Yes ingh auth login, rewrites it. - Inside the repo,
git config user.namemust match aghlogin, the same as forghitself.
command -v ghmust print~/.local/bin/gh. If it prints/usr/bin/gh, yourPATHorder is wrong or your shell cached the old path — runhash -rand open a new terminal.- Make sure the repo's
git config user.nameexactly matches aghlogin (case-sensitive).gh auth statuslists the valid logins. - A
GH_TOKENalready in your environment wins over the wrapper.echo ${GH_TOKEN:+set}printssetif one is there.
GitHub gives this answer, not "forbidden", when the account making the call can't see a private repo. So gh ran as the other account.
The wrapper picks the account from the directory gh runs in, not from the repo you name with -R owner/repo or a URL. A script or server that runs gh pr edit https://github.com/your-org/work-repo/pull/1 from its own folder in a personal repo acts as your personal account.
Run gh from inside the target repo's clone. In a script, cd there first or set the child process's working directory.
git config user.emailinside the repo tells you what's active.git config --show-origin user.nameshows which file set it. A.git/configentry means the repo overrides yourincludeIfrule.- Remember
includeIfmatches on the trailing-slash directory:gitdir:~/Projects/github/personal-username/(with the slash) covers everything inside it. - Already committed with the wrong identity? Fix the config, then
git commit --amend --reset-authorfor the latest commit.
This is exactly why we used a PATH wrapper instead of a shell function — as long as ~/.local/bin is on the PATH that the script inherits, the wrapper applies. If a cron/CI shell has a minimal PATH, add ~/.local/bin to it explicitly.
Wrapping up
The whole setup comes down to letting the directory decide who you are:
includeIfon~/Projects/github/<owner>/swaps your commit identity based on who owns the repo.- A small
ghwrapper hands eachghcall the token of that same identity. - Git's credential helper goes through the wrapper, so clone, pull and push over HTTPS follow the directory too. No SSH keys.
Set it up once on each new machine.