This is the one place gitwho is genuinely weaker than git-credential-manager, and it is worth stating plainly rather than leaving implied.
The gap
identity.key and secrets.age sit in the same directory, both owned by the user. Any process running as that user reads both and decrypts. The encryption is therefore nominal against a local-process threat — a malicious npm postinstall, a compromised dev tool.
Worse than that, local code need not go near the key or the ciphertext. It can just ask:
printf 'protocol=https\nhost=github.com\npath=Org/repo.git\n' | gitwho credential get
and get password=<token> back. The credential helper is a decryption oracle for its own store by construction — being one is the entire job.
git-credential-manager stores in the login Keychain, where a different application must be ACL-authorised or macOS prompts the user. That barrier is what gitwho lacks. FileVault covers powered-off, not this.
Why the Keychain backend is implemented but not the default
KeychainBackend exists and sits behind the same Backend trait. It was abandoned during development for a measured reason: macOS keys a Keychain ACL to the calling binary's designated requirement, and for an unsigned binary that is its code hash. Writing an entry with one build and reading it with the next hung on a GUI prompt until killed. On a credential helper that runs on every git transport operation, that is fatal.
The binary is currently ad-hoc signed with no TeamIdentifier, so its ACL identity changes on every rebuild.
The fix, in order
- Sign the installed binary with a stable Apple Development identity, so the designated requirement survives rebuilds.
- Re-run
examples/keychain_probe.rs — it writes an entry with one build, rebuilds, and reads with the next. That probe exists precisely to answer this and takes about a minute. If a stable signature fixes the prompt it passes; if it does not, the Keychain stays off the table and the gap has to be closed another way.
- If it passes, switch the default backend on macOS. It is a default change plus a migration path from the existing age store — not a rewrite.
Step 2 is not optional and must not be skipped on the grounds that step 1 "should" work. A valid signature does not prove the ACL survived; only the probe does.
Worth evaluating alongside
age-plugin-se encrypts to Apple's Secure Enclave, which would give hardware-backed keys without the per-binary ACL problem — possibly a better fit than the Keychain, since the failure mode above is specifically about per-application ACLs. Should be measured against the same latency budget before being preferred.
- Whatever is chosen, the read stays on the hot path: single-digit milliseconds, with the existing regression test that pins secret reads under 100 ms.
What this does not fix
Nothing here stops code running as you from calling gitwho credential get. A Keychain ACL raises the cost of reading the store at rest; it does not remove the oracle. Any claim otherwise would be overselling it — see SECURITY.md.
This is the one place gitwho is genuinely weaker than git-credential-manager, and it is worth stating plainly rather than leaving implied.
The gap
identity.keyandsecrets.agesit in the same directory, both owned by the user. Any process running as that user reads both and decrypts. The encryption is therefore nominal against a local-process threat — a malicious npm postinstall, a compromised dev tool.Worse than that, local code need not go near the key or the ciphertext. It can just ask:
and get
password=<token>back. The credential helper is a decryption oracle for its own store by construction — being one is the entire job.git-credential-manager stores in the login Keychain, where a different application must be ACL-authorised or macOS prompts the user. That barrier is what gitwho lacks. FileVault covers powered-off, not this.
Why the Keychain backend is implemented but not the default
KeychainBackendexists and sits behind the sameBackendtrait. It was abandoned during development for a measured reason: macOS keys a Keychain ACL to the calling binary's designated requirement, and for an unsigned binary that is its code hash. Writing an entry with one build and reading it with the next hung on a GUI prompt until killed. On a credential helper that runs on every git transport operation, that is fatal.The binary is currently ad-hoc signed with no TeamIdentifier, so its ACL identity changes on every rebuild.
The fix, in order
examples/keychain_probe.rs— it writes an entry with one build, rebuilds, and reads with the next. That probe exists precisely to answer this and takes about a minute. If a stable signature fixes the prompt it passes; if it does not, the Keychain stays off the table and the gap has to be closed another way.Step 2 is not optional and must not be skipped on the grounds that step 1 "should" work. A valid signature does not prove the ACL survived; only the probe does.
Worth evaluating alongside
age-plugin-seencrypts to Apple's Secure Enclave, which would give hardware-backed keys without the per-binary ACL problem — possibly a better fit than the Keychain, since the failure mode above is specifically about per-application ACLs. Should be measured against the same latency budget before being preferred.What this does not fix
Nothing here stops code running as you from calling
gitwho credential get. A Keychain ACL raises the cost of reading the store at rest; it does not remove the oracle. Any claim otherwise would be overselling it — see SECURITY.md.