Skip to content

fix(gpg): Point pinentry-program at a real binary, split conf per-platform - #2

Open
triwats wants to merge 2 commits into
masterfrom
fix/gpg-pinentry-cross-platform
Open

fix(gpg): Point pinentry-program at a real binary, split conf per-platform#2
triwats wants to merge 2 commits into
masterfrom
fix/gpg-pinentry-cross-platform

Conversation

@triwats

@triwats triwats commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Found while secret-scanning #1. Unrelated to that PR, so raising separately.

The bug

gpg-agent.conf had pinentry-program pointing at the config file itself:

pinentry-program /Users/triwats/src/dotfiles/gpg-agent.conf

That's not an executable, so gpg-agent cannot spawn a pinentry at all:

$ printf 'BYE\n' | /Users/triwats/src/dotfiles/gpg-agent.conf
permission denied

$ printf 'BYE\n' | /opt/homebrew/bin/pinentry-mac
OK Pleased to meet you          # what it should do

With commit.gpgsign = true and enable-ssh-support in play, this breaks commit signing and gpg-agent-backed SSH the moment the passphrase cache expires or the agent restarts. It went unnoticed only because the running agent still held the pre-corruption config in memory — the on-disk config was already broken.

This was an uncommitted local edit, so it never reached master. It looks like a bad find/replace.

The fix

Restore a real pinentry path. The correct path is platform-specific, and a single shared file can't cover both:

  • gpg-agent.conf has no include directive
  • pinentry-program requires a literal absolute path — no ~ or $HOME expansion

So the file is split:

File pinentry
gpg-agent-linux.conf /usr/bin/pinentry-gnome3
gpg-agent-macos.conf /opt/homebrew/bin/pinentry-mac

The two are otherwise byte-identical, and the rename is a pure rename (R100) — the linux variant matches the previously committed content exactly, so the diff is only the rename plus the new macOS file.

install.conf.yaml picks between them using dotbot's native if: (supported in the vendored v1.20.1):

- link:
    ~/.gnupg/gpg-agent.conf:
      if: '[ "$(uname -s)" = "Darwin" ]'
      path: gpg-agent-macos.conf

These are separate link: blocks on purpose — both map the same destination, which would collide as a duplicate YAML key inside one block (last-wins, silently).

Verification

$ ./install
Creating link ~/.gnupg/gpg-agent.conf -> .../gpg-agent-macos.conf
Skipping ~/.gnupg/gpg-agent.conf          # linux variant correctly skipped
  • Linked pinentry completes an Assuan handshake (OK Pleased to meet you)
  • Agent reloaded via gpg-connect-agent reloadagent, passphrase cache confirmed empty, then this PR's own commit signed successfully — so pinentry-mac was genuinely invoked end-to-end, not served from cache:
    gpg: Good signature from "Tristan Watson (github) <tristan@triwats.com>" [ultimate]
    

Known limitations / follow-ups

  1. Intel Macs — Homebrew's prefix is /usr/local, not /opt/homebrew. gpg-agent-macos.conf would need adjusting there. The uname test can't distinguish; a [ -x /opt/homebrew/bin/pinentry-mac ] test could, if you ever run both.
  2. log-file /var/log/gpg-agent.log is unwritable/var/log is root-owned on both macOS and Linux, so with debug-level advanced set, gpg-agent is trying to write a log it has no permission to create. This is pre-existing and platform-independent, so I deliberately left it out of this PR to keep the rename clean. Worth a follow-up: either drop the two debug lines or point the log at a writable path.

Interaction with #1

Independent in content — different files, both branched from master — but they did initially conflict in install.conf.yaml.

My first revision rewrote that file wholesale, including its trailing whitespace, and #1 inserts its entry directly above that same whitespace. git merge-tree flagged it. Fixed in a follow-up commit that rebuilds the file from master byte-for-byte and changes only two well-separated hunks.

Now verified clean:

$ git merge-tree --write-tree add-claude-statusline fix/gpg-pinentry-cross-platform
exit=0

The merged tree was inspected directly and contains both the statusline link entry and both conditional gpg blocks, with the old entry removed. Merge order doesn't matter.

triwats added 2 commits August 7, 2026 16:23
…tform

pinentry-program had been corrupted to point at the config file itself:

    pinentry-program /Users/triwats/src/dotfiles/gpg-agent.conf

That is not an executable, so gpg-agent cannot spawn a pinentry at all:

    $ printf 'BYE\n' | .../gpg-agent.conf
    permission denied

With commit.gpgsign=true and enable-ssh-support, that breaks commit signing
and gpg-agent-backed SSH the moment the passphrase cache expires or the
agent restarts. It survived unnoticed only because the running agent still
held the pre-corruption config in memory.

Fixed by restoring a real pinentry path. Because the correct path is
platform-specific, and gpg-agent.conf has no `include` directive while
pinentry-program requires a literal absolute path (no ~ or $HOME
expansion), a single shared file cannot work across both platforms. So:

  gpg-agent.conf -> gpg-agent-linux.conf   /usr/bin/pinentry-gnome3
                    gpg-agent-macos.conf   /opt/homebrew/bin/pinentry-mac

The two files are otherwise byte-identical; the rename is a pure rename
(the linux variant matches the previous committed content exactly).

install.conf.yaml selects between them with dotbot's `if:` on uname. They
are separate `link:` blocks on purpose: both map the same destination,
which would collide as a duplicate YAML key inside a single block.

Verified: ./install links the macos variant and skips the linux one on
this machine; the linked pinentry completes an Assuan handshake.

Note for Intel Macs: Homebrew's prefix is /usr/local, not /opt/homebrew,
so gpg-agent-macos.conf will need adjusting there.
…with #1

The previous revision rewrote install.conf.yaml wholesale, including its
trailing whitespace. #1 inserts its link entry directly above that same
trailing whitespace, so the two branches overlapped there and
`git merge-tree` reported a conflict in this file.

Rebuilt from master byte-for-byte, changing only two well-separated
hunks: remove the old gpg-agent.conf entry, and append the conditional
blocks after the existing tail. No functional change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant