How to Use Work & Personal GitHub Accounts on Ubuntu (HTTPS + gh CLI)

A complete, ordered guide to running multiple GitHub accounts on a fresh Ubuntu machine. The folder picks the account for commits, gh and git over HTTPS, with no SSH keys.

Prerequisites

You'll need these on your Ubuntu machine before starting:

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
The trailing slash in 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.name for the current repo, which your includeIf rule sets.
  • gh auth token -u <name> prints that account's token from the keyring. If no login matches, it fails and gh falls back to the active account.
  • The token goes into GH_TOKEN for this one gh process only. gh reads GH_TOKEN before the active account, and nothing else on the machine sees it.
  • gh auth login, switch and status are skipped, because they refuse to run while GH_TOKEN is set. gh auth git-credential still 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 gh on PATH that isn't itself, so it never calls itself in a loop and needs no hard-coded path to the real binary.
An earlier version of this wrapper ran 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?

  1. Add the helper lines from Step 3.
  2. Log in to both accounts again as in Step 4. That switches gh to HTTPS and adds the workflow scope.
  3. In each clone, point origin at 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

Wrapping up

The whole setup comes down to letting the directory decide who you are:

  • includeIf on ~/Projects/github/<owner>/ swaps your commit identity based on who owns the repo.
  • A small gh wrapper hands each gh call 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.