Not "read the config files and hope". credoctor makes the real calls — SSH greetings, remote fetches, per-token org access — and tells you which of your identities is lying to you.
npx credoctor doctor
Nothing to clone. Node 20+. Tested on Linux, macOS and Windows in CI.
These four failures happened while setting up five separated trees on one Mac. Every config file involved was right. Nothing a config-inspecting tool reads had changed. Each one was only visible by asking the other side.
An SSH key sat on the right account, but the org enforces SAML SSO and the key was never authorized for it.
~/.ssh/config is perfect. Only a real fetch fails.
One gh auth login wrote an unscoped github.com credential into the
keychain and silently broke another tree's remotes.
A repo's local user.email quietly overrode the tree's
includeIf, so commits went out under a stale identity.
An OAuth token was SSO-authorized while the matching SSH key was not. They authorize separately, and nothing local records either.
Two independent remote states, both invisible.The unit that matters isn't "a git identity" — it's a credential domain: everything that must agree for one directory tree, plus the remote-side state no local file records. Verify reality, not configuration.
$ npx credoctor doctor credoctor doctor — 7 tree(s), config ./credoctor.json client pass identity you-at-client <you@client.example> pass no-local-override no repo overrides the tree identity pass signing-key-email key 9F0E1D2C… is bound to you@client.example pass remote-alias 6 repo(s) all use git@github-client pass ssh-identity greeted as you-at-client pass remote-reachable ~/code/client/platform reachable pass gh-account you-at-client (~/.config/gh-client) pass gh-org-access reaches 6 repo(s), e.g. clientco/platform pass gh-org-isolation denied all 10 repo(s) belonging to other trees
That last line is the point. It didn't read a config file — it asked GitHub, with that token, whether it could reach another tree's repositories, and confirmed it couldn't.
| Check | Kind | Catches |
|---|---|---|
| Tree identity resolves | local | A missing or mis-scoped includeIf |
No local user.email override | local | The override that silently wins over includeIf |
| No identity outside a tree | local | Commits stamped you@hostname in a stray clone |
| Signing key bound to tree email | local | "Unverified" commits; a key reused across orgs |
| Signing key not expired | local | Signatures that quietly stop verifying |
| Remotes use the tree's SSH alias | local | A remote falling back to the shared keychain credential |
| SSH greeting per alias | network | Wrong key, unregistered key |
| Real reachability | network | SSO-unauthorized key — the failure config cannot show |
gh resolves per store | network | Store / keyring mismatch |
| Per-token org access and denial | network | Token drift; over-broad access |
| No two trees share an alias, store or key | local | The coupling that makes one login break another tree |
No unscoped github.com credential | local | The keychain entry one gh auth login overwrites |
| Browser profile maps to the tree | local | Links opening as the wrong identity |
★ The differentiated three. They are the failures that cost real time, and no existing tool tests them. Full catalogue and reasoning in DESIGN.md.
npx credoctor import --write credoctor.jsonDraft a config from the includeIf blocks already in your ~/.gitconfig.npx credoctor doctorCheck every tree. Exits non-zero on any failure, so it fits a shell hook or CI.npx credoctor doctor --offlineLocal checks only, no network call.npx credoctor doctor --jsonMachine-readable output.
import infers each tree's SSH alias and orgs from the remotes actually in use. You
shouldn't have to hand-write a config describing a machine you already set up. A failure prints
the remedy, not just the fact — for an SSO-unauthorized key, the settings URL and the
exact button, because that is the step people miss.
doctor and import only. Provisioning (credoctor add,
credoctor apply) writes to ~/.gitconfig, ~/.ssh/config and
the keychain. That blast radius isn't worth taking until the checks have earned trust.
Non-GitHub hosts. Bitbucket and GitLab remotes are detected, but their auth isn't understood — they surface as a generic reachability failure.
Browsers. The check covers the Chromium family (Chrome, Chromium, Edge,
Brave), which share one Local State format. Firefox and Safari aren't read yet.
credoctor never reads, prints or moves a secret — it asks subsystems whether they work. It also
invokes binaries directly, never through a shell, because a gh() wrapper that forces
GH_CONFIG_DIR from $PWD will otherwise override the environment
credoctor sets and report the shell's opinion instead of the store you asked about.
Every release from v0.1.1 onward is published from CI by npm trusted publishing — no token exists in the repository — and carries a provenance attestation tying the tarball to a commit, a tag and a workflow run. You do not have to take that on faith:
npm audit signaturesVerifies the registry signature and the provenance attestation of what you installed.v0.1.0 was hand-published before that pipeline existed and carries no attestation. Releases from v0.1.1 on do. The mechanics, and the trap that cost the first attempt, are written up in RELEASING.md.