git-crypt + Trezor: keeping your repo key off the disk entirely

cyb3ralbert · 2026-07-19 · ← other posts

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.

git-crypt (GPG mode) → GPG → trezor-agent → Trezor

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
Gotcha: 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

ComponentVersion
TrezorTrezor One, firmware 1.12.1
GPG2.2.40
trezor_agent / libagent0.13.0
git-crypt0.7.0
OSDebian 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.