Manage multiple git identities — work, personal, open-source — so the right name, email, and SSH key are always used without thinking about it.
If you only have one GitHub account, git just works. But the moment you have two — a work account and a personal one — things get quietly broken in ways that are hard to notice.
You push a commit from your work laptop and it shows up under your personal email. Or you try to push to a company repo and get Permission denied (publickey). Or you've been signing commits for months and only realize the wrong key was used when someone audits the history.
The root cause is two separate problems that git and SSH don't solve for you automatically:
- Identity —
user.name,user.email, and the commit signing key. Per-repogit configis tedious and easy to forget. - SSH key routing — which private key SSH uses for a given host. If you have multiple keys, SSH may grab the wrong one from your agent. Adding
IdentitiesOnly yesto~/.ssh/confighelps, but configuring it per-host breaks down when both accounts are ongithub.com.
gitprofile solves both, permanently, for all repos in a directory — without touching ~/.ssh/config or rewriting remote URLs.
gitprofile uses git's built-in includeIf directive. When you add a profile, it writes a small block to your ~/.gitconfig:
# >>> gitprofile managed >>>
[includeIf "gitdir:~/work/"]
path = ~/.config/gitprofile/profiles/work.gitconfig
[includeIf "gitdir:~/personal/"]
path = ~/.config/gitprofile/profiles/personal.gitconfig
# <<< gitprofile managed <<<Each profile file holds the full identity and SSH config for that account:
# ~/.config/gitprofile/profiles/work.gitconfig
[user]
name = Jane Dev
email = jane@company.com
signingkey = ~/.ssh/id_work.pub
[core]
sshCommand = "ssh -i ~/.ssh/id_work -o IdentitiesOnly=yes"
[commit]
gpgsign = true
[gpg]
format = sshGit loads this file automatically for any repo under ~/work/. No per-repo setup, no environment variables, no aliases — it just works because git itself is doing the routing.
The IdentitiesOnly=yes in sshCommand is the key detail: it tells SSH to only try the key you specified and ignore everything in the agent. This is what prevents the "authenticated as wrong account" failure mode that trips up most manual setups.
brew install meanii/gitprofile/gitprofilego install github.com/meanii/gitprofile@latestcurl -LO https://github.com/meanii/gitprofile/releases/latest/download/gitprofile_linux_amd64.deb
sudo dpkg -i gitprofile_linux_amd64.debcurl -LO https://github.com/meanii/gitprofile/releases/latest/download/gitprofile_linux_amd64.pkg.tar.zst
sudo pacman -U gitprofile_linux_amd64.pkg.tar.zstRequires Go 1.21+.
git clone https://github.com/meanii/gitprofile.git
cd gitprofile
go build -o gitprofile .
mv gitprofile /usr/local/bin/# bash
gitprofile completion bash > /etc/bash_completion.d/gitprofile
# zsh
gitprofile completion zsh > "${fpath[1]}/_gitprofile"
# fish
gitprofile completion fish > ~/.config/fish/completions/gitprofile.fishHere's a real-world setup from scratch: a work account on GitHub and a personal one on the same host.
$ gitprofile add work
Full Name: Jane Dev
Email: jane@company.com
Host (github.com / gitlab.com / bitbucket.org): github.com
Generate new SSH key? (y/n) [y]: y
Generated ~/.ssh/id_work
Public key (add this to https://github.com/settings/ssh/new):
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
Enable SSH commit signing? (y/n) [y]: y
Directories this profile owns? (comma-separated, blank = none): ~/work
Profile "work" created — active for repos under: ~/work
Copy the public key and add it to GitHub. Do this for both authentication and signing (GitHub calls them "Authentication keys" and "Signing keys" separately).
$ gitprofile add personal
Full Name: Jane
Email: jane@personal.dev
Host (github.com / gitlab.com / bitbucket.org): github.com
Generate new SSH key? (y/n) [y]: y
Generated ~/.ssh/id_personal
Public key (add this to https://github.com/settings/ssh/new):
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5BBBB...
Enable SSH commit signing? (y/n) [y]: y
Directories this profile owns? (comma-separated, blank = none): ~/personal
Profile "personal" created — active for repos under: ~/personal
$ gitprofile list
* work: Jane Dev <jane@company.com> (dirs: [~/work])
personal: Jane <jane@personal.dev> (dirs: [~/personal])The * marks which profile is active in your current directory.
$ cd ~/work
$ gitprofile doctor
── profile: work ──
[OK] SSH key ~/.ssh/id_work
[OK] gitconfig fragment
Testing SSH auth → github.com ...
[OK] SSH auth
Hi jane-work! You've successfully authenticated...
[OK] allowed_signers entry
[OK] All checks passed.If you see Permission denied from the SSH test, the public key isn't added to GitHub yet — the doctor output will show you the URL to open.
This is the normal path. Once a profile owns a directory, every repo inside it inherits the right identity and key with zero effort.
mkdir -p ~/work
cd ~/work
git clone git@github.com:company/api.git # uses work SSH key automatically
cd api
git commit -m "fix: handle null case" # commits as jane@company.com
git push # authenticates as your work accountNo extra steps. Git picks up the profile because of the includeIf rule.
When you clone, the repo doesn't exist yet so includeIf can't fire. Use gitprofile clone instead — it sets GIT_SSH_COMMAND for that one command and drops the repo into the right directory:
gitprofile clone git@github.com:company/new-service.git --as work
# Clones into ~/work/new-service with the work SSH key
# Future git operations in that repo use the work profile automaticallyOr just run it from inside the watched directory and it auto-detects:
cd ~/work
gitprofile clone git@github.com:company/new-service.git
# Using profile "work" (matched current directory)You cloned something into /tmp or got handed a repo that lives outside your normal folders. Use gitprofile use to apply a profile just for that repo:
cd /tmp/client-hotfix
gitprofile use work
# Profile applied locally to this repo.
git log --oneline -1
# abc1234 fix: payment timeout — Jane Dev <jane@company.com>This writes the identity and SSH config into the repo's .git/config so it only affects that one repo and doesn't touch anything else.
You mostly use one account and only occasionally work on things that need a different one. Set a global catch-all:
gitprofile add personal --globalThis adds an unconditional [include] to your ~/.gitconfig that loads the personal profile everywhere. Directory-specific profiles still override it — the global one is just the fallback for anything that doesn't match.
gitprofile list
work: Jane Dev <jane@company.com> (dirs: [~/work])
* personal: Jane <jane@personal.dev> (global)Run gitprofile current from anywhere to see exactly which profile applies and why:
# Inside a watched directory
$ cd ~/work/api
$ gitprofile current
Profile: work (Jane Dev <jane@company.com>)
Reason: in directory ~/work (owned by profile)
# Inside a repo with a local override
$ cd /tmp/client-hotfix
$ gitprofile current
Profile: work (Jane Dev <jane@company.com>)
Reason: local override in .git/config
# Somewhere with no match and no global
$ cd /tmp/random
$ gitprofile current
No profile matches this directory. Use 'gitprofile use <name>' for a local override.You have a personal GitHub account but want to contribute to open source under that identity from wherever the repos happen to live. Either:
- Add
~/opensourceto your personal profile's dirs, and always clone there - Or set personal as
--globaland override withgitprofile use workinside work repos
gitprofile clone git@github.com:some-org/cool-project.git --as personal
cd ~/personal/cool-project
git commit -m "feat: add dark mode support"
# Commits as jane@personal.dev, signed with your personal keyYou're about to push and want to confirm you're using the right account:
$ gitprofile whoami
Jane Dev <jane@company.com>
Signing key: /Users/jane/.ssh/id_work.pubOne command, no arguments, no flags. Shows exactly what git will put in the commit.
Export your profiles and commit them to your dotfiles repo:
gitprofile export ~/dotfiles/gitprofile.yaml
git -C ~/dotfiles commit -am "update gitprofile config"On a new machine, after copying your SSH keys over:
gitprofile import ~/dotfiles/gitprofile.yaml
# Imported 3 profiles.
# Warning: SSH key not found at ~/.ssh/id_work — copy it to this machine before using profile "work"The import restores all profile metadata and wires up ~/.gitconfig. You just need the actual key files present.
If you use a passphrase on your keys (you should), load them into the agent once at login:
gitprofile agent add work --ttl 8h
gitprofile agent add personal --ttl 8hThe keys expire automatically after 8 hours so you're not leaving them loaded overnight.
If you use GitHub for work and GitLab for personal, or have a self-hosted Bitbucket, each profile just carries a different host:
gitprofile add gitlab-personal
# Host: gitlab.com
# ...
gitprofile add self-hosted
# Host: git.mycompany.internal
# ...The sshCommand in each profile is scoped to that profile's key, so there's no conflict even if both hosts are on the same machine.
Creates a new profile. Walks you through name, email, host, SSH key (generate new or point at existing), commit signing, and which directories it owns.
gitprofile add work
gitprofile add personal --global # catch-all default, no directory neededAfter running this, add the printed public key to your git host. The output includes the exact URL.
Shows all profiles. The * marks whichever one is active in your current directory.
$ gitprofile list
* work: Jane Dev <jane@company.com> (dirs: [~/work ~/company])
personal: Jane <jane@personal.dev> (global)
oss: J <j@oss.dev> (dirs: [~/oss])Tells you which profile is active right now and why. Useful when something feels off or you're not sure which identity a repo is using.
$ gitprofile current
Profile: work (Jane Dev <jane@company.com>)
Reason: in directory ~/work (owned by profile)Applies a profile as a local override inside the current repo's .git/config. Only affects that one repo. Use this for repos that live outside any owned directory.
cd /some/random/repo
gitprofile use personalClones with the right SSH key from the first byte. Without --as, auto-detects the profile from the current directory.
gitprofile clone git@github.com:company/service.git --as work
# Clones into ~/work/service (profile's first owned dir)
cd ~/personal
gitprofile clone git@github.com:you/side-project.git
# Auto-detects personal profile from the current directoryPrints the public key to paste into GitHub / GitLab / Bitbucket.
$ gitprofile key show work
Public key for work:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...Prints the key file paths — useful when a tool asks for the key location.
$ gitprofile key path work
Private: /Users/jane/.ssh/id_work
Public: /Users/jane/.ssh/id_work.pubInteractively update any field. Press Enter to keep the current value. Re-renders the gitconfig fragment and updates ~/.gitconfig when done.
gitprofile edit work
# Full Name [Jane Dev]: ← press Enter to keep
# Email [jane@company.com]: ← press Enter to keep
# SSH key [~/.ssh/id_work]: ← press Enter to keep
# ...Removes the profile, its gitconfig fragment, and its includeIf entry from ~/.gitconfig. Asks whether to delete the SSH key files too (defaults to keeping them).
gitprofile remove work
# Delete SSH key files? (y/n) [n]: n
# Profile removed.Shows the git identity that applies right now — resolved exactly as git would for the current directory. Faster than running git config user.email manually, and shows everything at once.
$ cd ~/work/api
$ gitprofile whoami
Jane Dev <jane@company.com>
Signing key: /Users/jane/.ssh/id_work.pubTells you which profile is active right now and why. Useful when something feels off or you're not sure which identity a repo is using.
$ gitprofile current
Profile: work (Jane Dev <jane@company.com>)
Reason: in directory ~/work (owned by profile)Use --short to print only the profile name — handy for shell prompt integration:
$ gitprofile current --short
workStarship prompt (~/.config/starship.toml):
[custom.gitprofile]
command = "gitprofile current --short"
when = "gitprofile current --short"
symbol = " "
style = "bold blue"Zsh PS1:
PS1='$(gitprofile current --short 2>/dev/null | sed "s/.*/ [&]/") %~ $ 'Renames a profile cleanly — updates the config, renames the fragment file, and rewrites the managed block in ~/.gitconfig.
gitprofile rename work main-work
# Renamed "work" → "main-work".
# Note: SSH key ~/.ssh/id_work was not moved — update it with 'gitprofile edit main-work' if needed.Duplicates a profile as a starting point. Copies identity fields (name, email, host, signing preference) and offers to generate a fresh SSH key. Directories are cleared so the two profiles don't conflict — set them with gitprofile edit <dest> afterwards.
gitprofile copy work work-client
# Generate new SSH key at ~/.ssh/id_work-client? (n = reuse ~/.ssh/id_work) [y]:
# Generated ~/.ssh/id_work-client
# Profile "work-client" created from "work".
# Set its directories with: gitprofile edit work-clientWrites all profiles to a portable YAML file (or stdout if no file is given). Key file paths are recorded but key contents are not — safe to commit to a dotfiles repo.
gitprofile export ~/dotfiles/gitprofile.yaml
# Exported 3 profile(s) to ~/dotfiles/gitprofile.yamlReads a file produced by gitprofile export and merges the profiles into the local config. SSH key files must already be present on the machine.
gitprofile import ~/dotfiles/gitprofile.yaml
# Imported 3 profiles.
gitprofile import ~/dotfiles/gitprofile.yaml --overwrite
# Replaces existing profiles that share a name.Adds a profile's SSH key to the running SSH agent. Useful at the start of a work session. Use --ttl to set an expiry so the key unloads automatically.
gitprofile agent add work
gitprofile agent add work --ttl 8h # expires after 8 hoursgitprofile agent list # show keys currently in the agent
gitprofile agent remove work # remove a profile's key from the agentInstalls a post-checkout hook that warns whenever you switch into a repo that has no active profile. Install once globally and it covers every repo on the machine.
gitprofile hook install --global
# Hook installed at ~/.config/gitprofile/hooks/post-checkout
# core.hooksPath → ~/.config/gitprofile/hooks (set in ~/.gitconfig)
# Every repo you check out will now warn if no gitprofile matches.Or install it only for the current repo:
gitprofile hook installRemove with gitprofile hook remove (or --global).
Runs a full health check. Catches the problems that silently break things:
- SSH key missing, wrong permissions, or not parseable
- gitconfig fragment missing or out of date
- Public key not registered in
allowed_signers(for signing profiles) - Live SSH connectivity test against each profile's host
- Two profiles claiming the same directory
- Managed block missing from
~/.gitconfig
$ gitprofile doctor
── profile: work ──
[OK] SSH key ~/.ssh/id_work
[OK] gitconfig fragment
[OK] allowed_signers entry
Testing SSH auth → github.com ...
[OK] SSH auth
Hi jane-work! You've successfully authenticated...
── profile: personal ──
[OK] SSH key ~/.ssh/id_personal
[OK] gitconfig fragment
[WARN] public key not in allowed_signers (~/.ssh/allowed_signers)
[OK] managed block present in ~/.gitconfig
1 problem(s) found. Run 'gitprofile doctor --fix' to auto-repair what's possible.Add --fix to automatically repair what it can — wrong key permissions, missing fragment files, missing allowed_signers entries, and a missing managed block:
gitprofile doctor --fixRun doctor after any setup change, or whenever a push fails and you're not sure why.
| Path | What it is |
|---|---|
~/.config/gitprofile/config.yaml |
The list of all your profiles |
~/.config/gitprofile/profiles/<name>.gitconfig |
Per-profile gitconfig fragment included by git |
~/.gitconfig |
Has the managed includeIf block appended |
~/.gitconfig.gitprofile.bak |
Backup of your gitconfig before each edit |
~/.ssh/id_<name> |
Generated ed25519 private key (permissions: 0600) |
~/.ssh/id_<name>.pub |
Generated ed25519 public key (permissions: 0644) |
~/.ssh/allowed_signers |
Used by git to verify signed commits locally |
gitprofile never touches anything outside these paths.
Your existing gitconfig is safe. All changes live inside a clearly-marked >>> gitprofile managed >>> block. The tool only reads and rewrites that block — everything else in your ~/.gitconfig is untouched.
A backup is written before every change. ~/.gitconfig.gitprofile.bak is created before any modification so you can restore it if something goes wrong.
Keys are never overwritten. If ~/.ssh/id_work already exists, add will ask if you want to reuse it. It won't silently clobber it.
IdentitiesOnly=yes is always set. Every profile's sshCommand includes this flag. It prevents SSH from offering other keys from the agent and accidentally authenticating as the wrong account.
Private key permissions are enforced. Keys are created as 0600. If you point at an existing key, gitprofile will fix its permissions if they're too open.
go test ./... # run all tests
go build -buildvcs=false . # build binary
just test # same via justfile
just check # fmt + vet + staticcheckTests cover config round-trips, gitconfig fragment rendering, managed-block rewriting, SSH key generation and validation, allowed_signers management, and hosts connectivity helpers.
MIT
