Pick the right git identity and credentials for a repository, automatically, wherever that repository lives on disk.
Working across several accounts — personal, two employers, a self-hosted Gitea —
means every repo needs a different author identity and different credentials
for whatever tooling touches it (gh, tea, an IDE, an MCP server). Getting it
wrong is quiet: you commit as the wrong person, or authenticate as the wrong
account, and nothing tells you.
The usual approaches key off filesystem path — includeIf "gitdir:" rules
plus per-directory environment loading. That breaks as soon as a repo isn't
where the rule expects: a clone made somewhere else, or a worktree placed in a
tool's own root. It breaks silently, falling back to whichever account is the
default.
Identity should follow the repository, not its location:
- git identity resolves from the remote URL (
includeIf "hasconfig:remote.*.url:"), so it is correct in a worktree, a relocated clone, or a temp directory. - git credentials come from a credential helper, which git hands the URL it is about to contact — so the right account is chosen even for a clone of a repository that does not exist on disk yet.
- CLI credentials are injected per invocation, into that process only.
ghandtearun through shims that resolve from the current repository and scrub every other account's variables first, so nothing sits in the ambient environment for an unrelated process to pick up.
One file declares your accounts. Nothing else needs editing when you add one.
[[accounts]]
name = "Work"
provider = "github"
login = "you-at-work" # the gh login; gitwho asks gh for the token
email = "you@example-corp.com"
match = ["github.com/example-corp/**"]provider decides which variables exec sets — a GitHub account gets
GH_TOKEN, a Gitea or Forgejo account gets GITEA_TOKEN and
GITEA_INSTANCE_URL — so nothing here declares a variable by name. match
runs against host/path, which is why several GitHub accounts on one host can
be told apart — by organisation, with no directory layout implied.
macOS and Linux, on x86-64 and arm64. Every route puts a gitwho binary in
~/.cargo/bin, so whichever you pick, that directory needs to be on your
PATH. You also need git ≥ 2.36 — includeIf "hasconfig:remote.*.url:"
landed there.
Homebrew
brew install DanielCarmingham/tap/gitwhoOne line, no package manager:
curl -LsSf https://github.com/DanielCarmingham/gitwho/releases/latest/download/gitwho-installer.sh | shCargo, if you already have a Rust toolchain:
cargo install gitwho # compiles it, a couple of minutes
cargo binstall gitwho # or grab the same prebuilt binary, secondsFrom source, which is what you want if you are going to change it:
git clone https://github.com/DanielCarmingham/gitwho
cd gitwho
cargo install --path . --lockedThe first three need no Rust toolchain at all. Whichever you use, pick the
location once and leave it alone: init, sync and shim install bake the
binary's absolute path into what they generate, so moving it later breaks that
config silently.
gitwho init # dry run: lists every step, writes nothing
gitwho init --write # creates ~/.config/gitwho (0700), scaffolds accounts.toml, stopsIt stops there on purpose, because the next part is the one thing it cannot do
for you: put your accounts in ~/.config/gitwho/accounts.toml. To get a head
start, read the repositories you already have:
gitwho init --discover ~/src # prints a proposal, writes nothingIt groups every remote.origin.url it finds by host/org and prints a config
you can pipe into a file. It cannot know which orgs are the same person, so
it says so rather than guessing — merging those blocks is the part left to you.
Then log gh/tea in as each account's login (gh auth login,
tea login add --url …) — exec reads the token from whichever CLI the
account's provider names, on demand, so there is no token to store. Finish
with:
gitwho init --write # finishes, and ends by running doctorinit is idempotent — re-run it whenever you add an account. Every step
reports ok when there was nothing to do, so a second run tells you exactly
what changed.
docs/INSTALL.md covers what init does, how to do each
piece by hand, the checks that prove it works, and the two test commands that
produce false passes.
Once a local repository has commits, one command creates it on the account's
server, points origin at it and pushes:
gitwho publish --account Work # github.com, via gh
gitwho publish --account SelfHosted # Gitea/Forgejo, via tea- The account is the one you name, or the one whose
pathsclaims the directory;gitwho accountslists the names. A repository with no remote gives gitwho nothing else to go on, and it will not publish as the default account on a guess. - Authorship is checked first. If any commit was authored by someone other
than the account's
email— typically your default identity, because the commit came before the remote did — it stops before creating anything and prints the git-filter-repo command that rewrites them to the account.--keep-authorspublishes them as they are instead. origincomes from the server's answer: the ssh URL for an account with ansshKey, the https URL otherwise (--ssh/--httpsto choose). A server's ssh host often differs from its web host, so it is never guessed.- The name defaults to the directory's;
--owner <org>creates it in an organisation; it is private unless--public.
If the push fails after the repository was created, gitwho leaves it in place and prints the command to finish.
To publish a copy of a repository that already has a remote — a mirror on
another account — add --remote <name>:
gitwho publish --account SelfHosted --remote mirror--account is required then, the author check is skipped (the history is
already published as origin's account), the branch keeps tracking origin,
and the remote uses https unless you pass --ssh: with two accounts in one
repository, the ssh key follows whichever account decides identity. Once origin exists, nothing needs naming
again — every later gh, tea, push and commit resolves from it.
A repository can have any number of remotes. A fork's upstream belongs to
someone else, matches none of your accounts, and changes nothing.
When remotes in one repository belong to two of your own accounts — a
fork of your employer's repo into your personal account, or a mirror on a
self-hosted server — each still authenticates as its own account, but commits
can carry only one identity. gitwho uses the account declared later in
accounts.toml. To change which one decides, move its [[accounts]] block
below the other and run gitwho sync --write; the order applies to every
repository those accounts share. gitwho whoami in such a repository names
the account that decides, and gitwho publish --remote says so when it
creates one.
gitwho init set everything up; safe to re-run
gitwho init --discover <roots> propose accounts.toml from repos on disk
gitwho doctor report whether the wiring is coherent (read-only)
gitwho whoami which account this repository resolves to, and why
gitwho accounts the accounts in accounts.toml; --quiet for names only
gitwho config edit open accounts.toml in git's editor, then validate it
gitwho sync regenerate the identity and credential rules
gitwho credential git credential helper
gitwho exec -- cmd run a command with exactly one account's credentials
gitwho publish create this repo on the account's server, set origin, push
gitwho shim install wrapper scripts for gh / tea
gitwho mcp sync route provider MCP servers through exec
doctor is the one to run after any change. It is read-only, exits non-zero on
problems, and never prints a secret value.
In use on the author's machine since 2026-08-10: git identity, git credentials,
the gh/tea shims and a wrapped MCP server all route through it, and direnv
no longer exports a token per directory. 273 tests, clippy clean.
One gap remains there, and doctor reports it rather than hiding it: a shell
rc file still exports GITEA_TOKEN, so interactive shells carry a copy that
nothing reads. That [ambient] warning is the exposure this tool exists to
remove — it is left visible on purpose.
Platform reality, stated precisely because the gap between these rows is easy to paper over:
| macOS | verified — this is where it runs every day |
| Linux | the test suite passes on x86-64 in CI. On Debian/aarch64 the whole init flow has also been driven by hand: 0700/0600 modes, identity resolution, the credential helper, a shim executed with the shim dir first on PATH, and the .bashrc branch |
| Windows | the %APPDATA% path, the .cmd shim and the PATHEXT lookup are unit-tested as pure functions from macOS, and have never run on Windows. Do not treat them as working |
What it does not cover yet. jj takes its credentials from git, so pushing
works — but it keeps author identity in its own config and does not read
gitconfig's includeIf rules, so in a colocated repo jj and git can commit
as different people without saying so. Details, and what fixing it would take,
are in docs/DESIGN.md.
The property that matters is not encryption — gitwho stores no secret at all.
It is that a token is fetched by the one process that needs it, at the
moment it needs it, instead of sitting in your environment where everything
you launch inherits it. exec clears every managed variable before setting
the resolved account's, so nothing ambient survives into the child.
An account names a provider and a login, never a value, which is what lets
accounts.toml itself be committed to a dotfiles repo — there is nothing
secret in it to leak. The directory it lives in is still 0700 and the file
0600, because a writable config is a redirect vector: whoever can write it
can add a match pattern for a host they control and be handed a token.
Tokens come from the provider's own CLI, read fresh on every invocation:
gh auth token --hostname github.com --user octocatThat is a pointer, not a copy, so it cannot go stale when you rotate the
token — the value stays wherever gh or tea already keeps it (the macOS
keychain for gh; tea's own credentials.json.enc, keyed by the keychain).
Values are never printed by gitwho (only fingerprints) and never enter argv.
If the CLI cannot answer, gitwho fails and says so; it never falls back to
another source or another account.
What it does not do: gitwho holds no secret, so there is nothing of its own
to defend. A token lives wherever gh/tea put it, owned by you, so anything
running as your user can read the same token gitwho would ask for.
Be concrete about that, because "gitwho decides who gets logged in" invites the
wrong conclusion. Anything running as you can run gh auth token directly and
get the same value gitwho would. Against local code running as you, gitwho
is not an improvement on asking gh or tea directly, and does not claim to
be. What changes is exposure over time: a token is present in one process for
one invocation, instead of in every process for the whole session.
The full threat model is in docs/DESIGN.md. To report a vulnerability, see SECURITY.md — please use private reporting rather than a public issue.
- docs/INSTALL.md — setting it up, step by step, with a check after each one and the traps that produce false passes.
- docs/DESIGN.md — why it is shaped this way: the measured evidence, the resolution algorithm, the threat model it does and does not cover, and the R1–R15 principles the source refers to by number.
- docs/accounts.toml.example — a commented starting config.