git-crypt + Trezor: keeping your repo key off the disk entirely
git-crypt is a solid tool for encrypting parts of a git repo transparently.
Its default mode, though, stores the symmetric key as a plaintext file on disk
at .git-crypt/keys/default. Anyone with read access to that file —
malware, a careless tar of the wrong directory, a compromised
backup — gets the key.
git-crypt also supports a GPG mode
(git-crypt add-gpg-user), where the symmetric key is encrypted to
a GPG identity's public key instead of sitting in the clear. Combine that with
trezor-agent (a
hardware-backed GPG/SSH/age agent for Trezor), and the private key material
never touches the host's disk at all — it's generated and lives on the
Trezor. The host only ever sees encrypt/decrypt results.
Nothing here is novel cryptography — it's three existing, well-tested
pieces wired together. What I couldn't find anywhere before doing this myself
was a documented end-to-end run of this specific combination, with
the actual UX questions answered: does git-crypt unlock ping the
Trezor once per session, or once per file? (Once per unlock call
— not per file, which matters a lot for whether this is usable day to
day, not just a neat demo.)
Why this is worth doing
The git-crypt key is encrypted to the Trezor identity's public key and stored
in .git-crypt/keys/. Decrypting it requires the Trezor to sign
the operation on-device. A compromised host can, at worst, prompt you to
confirm an operation on the physical Trezor screen — it cannot exfiltrate
the key material itself, because the private key was never on the host to
begin with.
The PIN matrix, explained properly
This tripped me up initially, so it's worth spelling out clearly. The PIN entry prompt (whether curses in a terminal or a GTK dialog) shows a fixed position map:
7 8 9
4 5 6
1 2 3
This is not the PIN, and it's not read in that visual order. The Trezor's own screen shows the same 3×3 grid, but with digits shuffled into random positions on every attempt. You look at the device screen, find where each real digit of your PIN physically sits, and type the corresponding position number from the map above. The computer never learns your actual PIN digits, only which positions you clicked/typed — and since the layout is randomized per attempt, a keylogger on the host learns nothing useful either. It's the exact same scheme Electrum's clickable PIN grid uses, just typed instead of clicked.
Setup summary
Full step-by-step version, including udev rules, VM USB passthrough, and pinentry gotchas, is in the skill below. Short version:
# 1. trezor-agent in a venv
python3 -m venv venv && source venv/bin/activate
pip install trezor_agent
# 2. init a hardware-backed GPG identity
trezor-gpg init "Name <email>" --pin-entry-binary=/usr/bin/pinentry-curses
# 3. wire it into git-crypt
git-crypt init
GNUPGHOME=~/.gnupg/trezor git-crypt add-gpg-user <fingerprint>
# 4. verify
git-crypt lock
GNUPGHOME=~/.gnupg/trezor git-crypt unlock # PIN + one Trezor confirmation, whole repo
trezor-gpg init and git-crypt
unlock both need a real interactive terminal — the PIN entry
program (pinentry-curses or a GUI variant) needs a real TTY or a
real X DISPLAY. Running this from a non-interactive script or
automation context without either will just throw an isatty
error.
Verified against
| Component | Version |
|---|---|
| Trezor | Trezor One, firmware 1.12.1 |
| GPG | 2.2.40 |
| trezor_agent / libagent | 0.13.0 |
| git-crypt | 0.7.0 |
| OS | Debian 12 |
Full skill / step-by-step
The complete, copy-pasteable version — udev rules, libvirt USB passthrough for when the Trezor lives on a different host than the repo, and notes on relevant upstream trezor-agent issues — is published as a Claude Code skill here:
cyb3ralbert/claude-skills — git-crypt-trezor-setup
Related: Trezor as a TOTP vault, using the same device for 2FA codes instead of a repo encryption key · Recovering this GPG identity from a seed phrase, no hardware needed.