Manage Git identities and route them to directories — using Git's own conditional includes, nothing else.
If you juggle several Git accounts (personal, work, clients…) on one machine,
git-id lets you keep no global identity at all and never run
git config user.email after a clone again: every repository picks the right
user.name / user.email (and signing settings) from the directory it lives
in.
$ git id create work --name "Jane Doe" --email jane@work.example
$ git id use work ~/dev/work
$ git clone git@github.com:your-org/some-repo ~/dev/work/some-repo
$ cd ~/dev/work/some-repo && git commit ... # commits as jane@work.exampleThe binary is named git-id, so Git picks it up as a native subcommand:
git id use work ≡ git-id use work.
git-id never rewrites your ~/.gitconfig. It owns exactly two things, both
under ${XDG_CONFIG_HOME:-~/.config}/git-id/:
~/.gitconfig ← yours; gets ONE include line, once (git-id init)
└─ include.path = ~/.config/git-id/routes.gitconfig
~/.config/git-id/routes.gitconfig ← owned & generated by git-id
└─ [includeIf "gitdir:/Users/you/dev/work/"]
path = "/Users/you/.config/git-id/identities/work.gitconfig"
~/.config/git-id/identities/work.gitconfig ← one fragment per identity
└─ [user] name / email / signingkey, [gpg] format,
[commit] gpgsign, [core] sshCommand
A route on a directory applies to it and every repository below it. Routes are stored canonicalized (symlinks resolved) with a trailing slash — the exact form Git matches against — and sorted parent-before-child, so the deepest route always wins (Git gives precedence to the last value found).
Operations are atomic (temp file + rename), idempotent (re-running use on a
routed directory replaces the route, never duplicates it), and the global
config is backed up to a timestamped .bak-* file before any modification.
Requires git ≥ 2.13. Works on macOS, Linux and Windows — paths are stored in
the canonical, forward-slash form Git matches gitdir: patterns against on
each platform, and every release is tested on all three.
With Homebrew (prebuilt binaries):
$ brew install jmarette/tap/git-idWith the standalone installer (no toolchain needed):
$ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/jmarette/git-id/releases/latest/download/git-id-installer.sh | shOn Windows (PowerShell):
> irm https://github.com/jmarette/git-id/releases/latest/download/git-id-installer.ps1 | iexOr from source:
$ cargo install --path .Make sure ~/.cargo/bin is on your PATH (it usually is), then:
$ git id initinit is one-time and idempotent: it creates the config directory, links the
routes file into your global git config, and offers to set
user.useConfigOnly=true — a recommended guard that makes Git refuse to
commit in any directory where no identity applies, instead of silently
guessing one from your hostname.
| Command | What it does |
|---|---|
git id init |
One-time setup (idempotent, backs up your gitconfig first). |
git id create <name> |
Create an identity (--name, --email, --signing-key, --sign, --format, --ssh-key/--ssh-command; prompts for the basics if omitted). |
git id list |
Identities and the directories routed to them (--paths for the dir → identity map). |
git id show <name> |
Full detail of one identity, including the raw fragment. |
git id edit <name> |
Update via flags, or open the fragment in $EDITOR when no flags are given. Hand-added keys survive. |
git id delete <name> |
Delete the identity and purge its routes (asks for confirmation; --force to skip). |
git id use <name> [dir] |
Route dir (default: current directory) and everything below it to an identity. Replaces any existing route for that directory. |
git id unset [dir] |
Remove the route of a directory. |
git id which [dir] |
Which identity applies here? Shows the matched route and what git actually resolves, and flags local overrides. Alias: current. |
git id doctor |
Sanity-check the whole setup (include line, fragments, stale routes, …). |
git id uninstall |
Undo git-id's setup (include line, useConfigOnly, config dir) before removing the binary. |
git id completions [shell] |
Print shell completions; completions install writes them into place for every shell on PATH (or just [shell]). Supports bash, zsh, fish, nushell, elvish, powershell. |
git id man |
Print the man page (roff). git id init already installs it, so man git-id and git id --help just work. |
Every command has a detailed --help.
An identity can carry its signing and transport settings, so they follow the directory like the name and email do:
$ git id create work --name "Jane Doe" --email jane@work.example \
--signing-key ABCDEF12 --sign --format ssh \
--ssh-key ~/.ssh/id_work--format <openpgp|ssh|x509>setsgpg.format(SSH signing needs git ≥ 2.34).--ssh-key <path>is shorthand: git-id writescore.sshCommand = ssh -i <path> -o IdentitiesOnly=yes, so that exact key is used and an agent key can't shadow it. Use--ssh-command "<cmd>"instead to store a full command verbatim (custom port, proxy, …).
On edit, --no-format and --no-ssh remove those settings; --signing-key ""
removes the key. Anything you add to a fragment by hand is preserved.
core.sshCommand only drives Git's own transport (clone, fetch, push). A
bare ssh -T git@gitlab.com ignores Git config and always uses your default
key, so to check a specific identity's key, point ssh at it explicitly:
$ ssh -i ~/.ssh/id_work -o IdentitiesOnly=yes -T git@gitlab.comThat per-identity key is what lets two accounts on the same host coexist. A given SSH key can only belong to one account (GitHub and GitLab reject a key already registered elsewhere), so give each account its own key and route them by directory:
$ git id create work --email you@work.example --ssh-key ~/.ssh/id_work
$ git id create perso --email you@perso.example --ssh-key ~/.ssh/id_perso
$ git id use work ~/dev/work
$ git id use perso ~/dev/persoA plain git push then uses the right account from wherever the repo lives.
list, show and which take --json:
$ git id which --json
{
"identity": "work",
"gitdir": "/Users/you/dev/work/some-repo/",
"route": "/Users/you/dev/work/",
"name": "Jane Doe",
"email": "jane@work.example",
"in_repo": true,
"effective": { "name": "Jane Doe", "email": "jane@work.example", "origin": "…/identities/work.gitconfig" },
"mismatch": false
}which exits non-zero when no identity applies, so it works as a guard in
scripts and hooks.
git id init already sets these up for every shell it finds on your PATH, so
usually there is nothing to do. To (re)install them yourself:
$ git-id completions install # every shell found on PATH
$ git-id completions install zsh # …or just one, named explicitly
$ git-id completions install --current # …or just the shell you're in
$ git-id completions install --activate # …and wire it into your startup fileIt writes each completion script where the shell autoloads it (skipping any
already up to date), so usually you just restart your shell. When a shell has no
writable autoload location — a system zsh whose $fpath is read-only, an older
nushell, or elvish/powershell — it falls back to a private location and prints
the one line to add to your startup file. Pass --activate to have git-id add
that line for you (idempotent, and removed by git id uninstall); without it,
git-id never edits your rc files. Supported shells: bash, zsh, fish, nushell,
elvish, powershell. git id doctor reports, per shell, whether its completion
file is installed.
Note (nushell): nushell completes the exact command name, so use
git-id <Tab>. git-id also shipsgit id <Tab>(the spaced form) externs for nushell; in zsh,git id <Tab>works through git's own completion dispatch.
Manual. git-id completions <shell> prints the script to stdout, so you
can place it yourself:
# bash
$ git-id completions bash > ~/.local/share/bash-completion/completions/git-id
# zsh — then add `fpath+=(~/.zfunc)` before compinit in ~/.zshrc
$ git-id completions zsh > ~/.zfunc/_git-id
# fish
$ git-id completions fish > ~/.config/fish/completions/git-id.fish
# nushell — then add `source ~/.config/nushell/completions/git-id.nu` to config.nu
$ git-id completions nushell | save -f ~/.config/nushell/completions/git-id.nu- Linked worktrees and submodules: Git matches
gitdir:against the real.gitdirectory, which lives under the main repository. A worktree placed under a routed directory whose main repo lives elsewhere keeps the main repo's identity.git id whichdetects and explains this. - Bare repositories route like any other: because routes are stored with a trailing slash, a route matches a bare repo whose git directory is the routed directory itself, as well as every repository below it.
- Symlinked paths are stored resolved (e.g.
/tmp/...becomes/private/tmp/...on macOS) because that is what Git matches against. git id --helpopens thegit-idman page;git id initinstalls it (andgit-id manprints it). Windows has no man pages — usegit-id --helporgit id <command> --helpthere.- Completions cover
git-id …invocations; completing throughgit id …would need a git-specific completion script (future work).
First let git-id undo everything it set up — the include.path line in your
global git config, the user.useConfigOnly guard, and the config directory:
$ git id uninstallThen remove the binary, depending on how you installed it:
- Homebrew:
brew uninstall git-id - cargo:
cargo uninstall git-id - standalone installer: delete
git-idfrom~/.cargo/bin(git-id.exeon Windows)
$ cargo test # unit + end-to-end tests (run in throwaway HOMEs)
$ cargo clippy --all-targets -- -D warnings
$ cargo fmt --checkThe integration tests never touch your real configuration: each scenario runs
the binary and a real git inside its own temporary HOME.
Notable changes are tracked in CHANGELOG.md.
Future work: routing by remote URL (hasconfig:remote.*.url), doctor --fix.
Contributions are welcome.
- For anything beyond a small fix, open an issue first so we can agree on the approach before you spend time on it. Small fixes can go straight to a pull request.
- Fork, branch off
master, and open your PR againstmaster. CI must be green — it runs rustfmt, clippy (warnings denied) and the full test suite on every pull request. - New features should come with tests and an entry under
## [Unreleased]inCHANGELOG.md.
Commit messages follow Conventional Commits
(feat:, fix:, docs:, refactor:, test:, chore:). Pull requests are
merged with a merge commit (not squashed), so every commit is preserved in
master — keep each commit focused and give it a Conventional Commits message.
Contributions are dual-licensed under MIT OR Apache-2.0; see License.
Releases are automated by cargo-dist:
pushing a v* tag builds the binaries, publishes the GitHub Release, and
updates the Homebrew formula in the tap.
To cut version X.Y.Z:
-
Bump
versioninCargo.tomland runcargo buildsoCargo.lockmatches — the tag and the package version must be equal, or the release fails. -
Rename the in-progress
CHANGELOG.mdsection to## [X.Y.Z](no date — it becomes the Release title), add its compare link, and open a fresh section for the next cycle. -
Commit, then tag and push:
$ git tag vX.Y.Z $ git push origin master vX.Y.Z
Never move or delete a published tag — the release assets and the Homebrew
formula point at it, so re-tagging breaks existing installs; ship a new
version instead. (The Release title can be changed afterwards without touching
the tag.) Release targets, installers and the tap live in
dist-workspace.toml; re-run dist init after changing
it, and dist plan to preview a release.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.