gitwho

Identity follows the repository, not the directory.

gitwho resolves your git identity and credentials from a repository's remote URL rather than where it sits on disk — so a worktree, a relocated clone or a temp directory still commits as the right person.

Where a path-based rule stops matchingThe same repository in three places. An includeIf gitdir rule matches only the first, ~/work/repo, and applies the acme identity, dev@acme.example. In the worktree at /tmp/wt-3 and in a second clone at ~/Downloads/repo no rule matches, so git falls back to the default personal identity, you@personal.dev. Git reports nothing in either case: no error, no warning, no non-zero exit. The commit is simply signed by the wrong person.includeIf "gitdir:"identity keyed to where the repo sits~/work/repoa normal clonedev@acme.examplerule matched/tmp/wt-3a worktree in a tool's rootyou@personal.devno rule matched · default~/Downloads/repoa clone made elsewhereyou@personal.devno rule matched · defaultNothing errors. Nothing warns. Nothing exits non-zero.The commit is simply signed by the wrong person.How gitwho resolves the same three casesThe same three locations. gitwho reads the repository's remote, github.com/acme/repo, which is identical in all three, and resolves one account from it: dev@acme.example. The worktree, the relocated clone and the temp directory all get the correct identity, because the path on disk is never consulted.gitwhoidentity keyed to the repository's remote~/work/repoa normal clone/tmp/wt-3a worktree in a tool's root~/Downloads/repoa clone made elsewheregithub.com/acme/reporeported by all threedev@acme.exampleone account, everywhereCorrect in a worktree, a relocated clone, or a tempdirectory. The path on disk is never consulted.

Quick start

Installing it is one line. The part that takes thought is the file it hands you afterwards — so the steps below go as far as a machine that doctor calls healthy, not just as far as a binary on PATH.

  1. Install the binary

    sh
    brew install DanielCarmingham/tap/gitwho

    or, with no package manager

    sh
    curl -LsSf https://github.com/DanielCarmingham/gitwho/releases/latest/download/gitwho-installer.sh | sh

    or, with a rust toolchain

    sh
    cargo install gitwho

    macOS and Linux, on x86-64 and arm64. Every route puts a gitwho binary in~/.cargo/bin, so that directory needs to be on your PATH. You also need git ≥ 2.36includeIf "hasconfig:remote.*.url:"landed there.

    Pick the location once and leave it alone: init bakes the binary's absolute path into everything it generates, so moving it later breaks that config silently.

  2. Run init, and let it stop

    One command, run twice, with your accounts written in between. The first is a dry run: it lists every step it would take and writes nothing.

    sh
    gitwho init
    gitwho init --write

    The second creates the store, scaffolds an empty accounts.toml, and halts:

    created       ~/.config/gitwho/identity.key (owner-only)
    created       ~/.config/gitwho/accounts.toml
    
    Now the part only you can do:
    
      1. edit ~/.config/gitwho/accounts.toml
         replace the example accounts with yours
      2. gitwho secret set <Account> <VAR>    once per token
      3. gitwho init --write                  re-run to finish

    It stops on purpose. Generating rules from a template of placeholders would give you a machine that looks configured and resolves every repository to an account that does not exist — working-but-wrong, which is the failure this tool exists to prevent. Nothing has gone wrong; step 3 is your half of it.

  3. Write your accounts

    Open ~/.config/gitwho/accounts.toml. It names environment variables and never holds their values, which is what lets it live in a dotfiles repo. A complete two-account config:

    toml
    [defaults]
    account = "Personal"
    gitName = "Your Name"
    
    [[accounts]]
    name = "Personal"
    provider = "github"
    email = "you@example.com"
    gitCredential = "GH_TOKEN"
    sshKey = "~/.ssh/id_ed25519_personal"
    match = ["github.com/your-personal-username/**"]
    env = ["GH_TOKEN"]
    paths = ["~/src/personal/"]
    
    [[accounts]]
    name = "Work"
    provider = "github"
    email = "you@acme.example.com"
    gitName = "Your Name (Acme)"
    gitCredential = "GH_TOKEN"
    match = [
        "github.com/acme-corp/**",
        "github.com/acme-labs/**",
    ]
    env = ["GH_TOKEN"]
    paths = ["~/src/work/"]

    match runs against host/path, which is why two GitHub accounts on one host can be told apart — by organisation, with no directory layout implied.

    Not your shape? Recipes has three more complete configs, andthe annotated template explains every field.

  4. Store the tokens, then finish

    sh
    gitwho secret set Personal GH_TOKEN
    gitwho secret set Work GH_TOKEN
    gitwho init --write

    secret set reads the value from stdin, so it never enters argv, because ps is public. Stored values are printed back only as fingerprints.

    If gh is already logged in as that account you can skip storing a token altogether — env = [{ var = "GH_TOKEN", from = "gh", user = "..." }]reads it from gh on demand, so there is no copy to keep in sync.Installing gitwho covers when that is worth doing.

    init is idempotent. Re-run it whenever you add an account; every step reportsok when there was nothing to do, so a second run tells you exactly what changed.

Then, after any change:

sh
gitwho doctor

A healthy run prints ok for every check and exits zero — the store's permissions are intact, every token the config names is present, and git's credential helper actually reaches gitwho. What that looks like is below.

The problem

Working across several accounts — personal, two employers, a self-hosted Gitea — means every repository needs a different author identity and different credentials for whatever 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 the filesystem pathincludeIf "gitdir:" rules plus per-directory environment loading. That breaks as soon as a repository is not where the rule expects: a clone made somewhere else, or a worktree placed in a tool's own root. It breakssilently, falling back to whichever account is the default.

So gitwho keys off the repository instead. Identity resolves from the remote URL, credentials come from a helper git hands the URL it is about to contact, and CLI credentials are injected into one process for one invocation. One file declares your accounts; nothing else needs editing when you add one.

What doctor reports

gitwho doctor is the one to run after any change. It is read-only, exits non-zero on problems, and never prints a secret value — only fingerprints — so its output is safe to paste when you are asking for help.

ok   [permissions] /Users/you/.config/gitwho and its files are owner-only
ok   [backend] secrets are in the age file at /Users/you/.config/gitwho/secrets.age, chosen by default
ok   [secrets] Personal/GH_TOKEN 4c1f9ab30e77
ok   [secrets] Work/GH_TOKEN 9e02b7d5164a
warn [ambient] GH_TOKEN is set in the environment; every process launched from this shell inherits it

That [ambient] line is left visible on purpose. It means a provider token is sitting in the environment, where every process launched from that shell inherits it — which is the exposure this tool exists to remove, not a check to quieten. The README reports the same warning against the author's own machine rather than hiding it.

One process, one invocation

The property that matters is not the encryption. 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.

The callersFour things need credentials for the same repository: git itself, the gh CLI, tea, and an MCP server.callersgitghteamcp serverall four need credentialsWhat gitwho resolves fromgitwho resolves from the repository's remote rather than its path on disk, reducing it to host and path — github.com/acme/repo — and it does this on every invocation rather than once per shell.gitwhothe repository's remote,not its path on diskgithub.com/acme/repothe key it matches onre-resolved per invocationThe account that matchesaccounts.toml declares every account with the host and path patterns it owns. github.com/acme/repo matches the acme account's pattern github.com/acme/**. The personal and gitea accounts do not match, so no directory layout is implied anywhere.accounts.tomlacmegithub.com/acme/**personalgithub.com/you/**giteagit.example.net/**matched on host/pathOne process, one tokengitwho runs the command with exactly one account's credentials. GH_TOKEN is set for this one process, from the account that matched. GITEA_TOKEN and GITEA_HOST — the variables the gitea account declares — are cleared before the process starts, shown struck through. What gets cleared is every variable named by any account in this config, so adding an account protects every other account from it. The token is present in one process for one invocation instead of in every process for the whole session, so nothing is left for an unrelated process to inherit.gh pr listGH_TOKENsetGITEA_TOKENclearedGITEA_HOSTclearedone process, one invocation.every other process sees nothing.

Platform reality

Stated precisely, because the gap between these rows is easy to paper over.

gitwho platform support, stated per platform
macOSverified — this is where it runs every day
Linuxthe test suite passes on x86-64 in CI. On Debian/aarch64 the whole init flow has also been driven by hand: store modes, identity, the credential helper, a shim executed for real, and the .bashrc PATH line
Windowsthe %APPDATA% path, the .cmd shim and the PATHEXTlookup are unit-tested as pure functions from macOS, and have never run on Windows. Do not treat them as working

One more gap, in the tool rather than the platform: 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 repository jjand git can commit as different people without saying so.

Reading