GPG agent and pass on this dotfiles setup

Short answer

pass stores each entry as a GPG-encrypted file. To read pass/neon-usage, pass calls gpg; gpg delegates the private-key operation to gpg-agent; the agent starts pinentry-curses when it needs the key passphrase and remembers the unlocked state for a limited time.

Today’s failure was most likely an agent/terminal connection problem, not a damaged password entry. The exact cause cannot be proven from the two GPG error lines alone.

What the recovery commands did

gpgconf --launch gpg-agent

Ask GnuPG to start its per-user agent if it is not already running. This is explicit startup; normally a GnuPG command can start the agent on demand too.

export GPG_TTY=$(tty)

Tell GnuPG which terminal is running the command. This matters for pinentry-curses, which must display the passphrase prompt on a real terminal. export makes the variable available to gpg, pass, and child processes. It affects the current shell only.

gpg-connect-agent updatestartuptty /bye

Connect to the existing agent and update the terminal associated with it. This repairs a common stale-TTY case: the agent was started from an old terminal, or was started before the current shell had the correct GPG_TTY.

pass show pass/neon-usage
mk neon

The first command verifies that GPG can decrypt the entry and may populate the agent’s passphrase cache. The second command runs setup/neon-usage-env.sh, which reads the entry and writes the generated credentials file.

The repository already performs the three setup actions in setup/neon-usage-env.sh before calling pass (lines 25–33). The script deliberately treats agent setup as best effort and reports failure when pass cannot read the entry.

What gpg-agent is

It is a per-user background daemon for GnuPG. It is not the encrypted password store and it is not the private key itself. Its jobs include:

The agent normally listens on sockets such as:

~/.gnupg/S.gpg-agent
~/.gnupg/S.gpg-agent.ssh

pass uses the normal GPG socket. pinentry-curses, configured in .gnupg/gpg-agent.conf, is a short-lived helper that displays the prompt; it is not the long-running agent.

The usual flow is:

pass -> gpg -> gpg-agent -> pinentry-curses -> terminal
                         |
                         -> private-key operation -> decrypted text

The decrypted text is returned to pass. In this project, mk neon then writes a plaintext ~/.config/neon/usage.env with mode 600; that is separate from the agent’s cache.

Is systemd running it?

GPG does not require systemd. It can start gpg-agent itself through its standard socket mechanism, and gpgconf --launch gpg-agent can request startup directly.

This machine’s Debian package also installs systemd user units:

Those units use socket activation: the user systemd manager opens the socket, and starts gpg-agent --supervised when a client connects. Therefore either of these can be true, depending on the login/session setup:

  1. GnuPG starts the agent on demand.
  2. systemd user socket activation starts it.

They are alternative lifecycle mechanisms, not two agents that should run simultaneously. systemctl --user status gpg-agent.socket gpg-agent.service shows the live systemd state in a normal desktop login.

This dotfiles repository has no GPG runit service. It does use runit for application services: .config/sx/sxrc starts runsvdir "$HOME/.config/sv", but no gpg-agent directory exists there.

Can runit manage it?

Yes in principle, but it is not the simple or necessary choice here.

GPG’s --supervised mode is designed for socket activation: systemd passes already-open sockets to the agent through file descriptors. Runit does not provide that protocol by default. Starting the ordinary daemon under runit also needs care because the normal daemon mode backgrounds itself; a naïve runit run script can make runit repeatedly start duplicate agents or collide with the existing socket.

A runit design would need to choose one owner, arrange the agent’s socket lifecycle, run it as the desktop user, and keep the terminal update step in each interactive shell. It must also disable the corresponding systemd user sockets or GnuPG auto-start path. That is more moving parts than this problem requires.

Recommendation

Keep the current design:

Useful diagnostics, without printing the secret:

gpgconf --list-dirs
gpg-connect-agent 'getinfo pid' /bye
gpgconf --kill gpg-agent
GPG_TTY=$(tty); export GPG_TTY
gpgconf --launch gpg-agent
gpg-connect-agent updatestartuptty /bye
pass show pass/neon-usage >/dev/null

If the problem returns, capture the output of the commands above and check whether the terminal is a real TTY. A missing secret key normally produces No secret key; the reported No such file or directory points more toward a missing/stale socket, terminal, pinentry, or runtime path, but needs an agent log to identify precisely.