← Back to blog

emacs · magit · gpg · git 3 Oct 2023 4 min read

Signing git commits from emacs with magit and pinentry


I’ve been using emacs for a while and been interested in magit, but I haven’t been able to create any commits due to GPG errors.

The setup

I have git configured to auto-sign commits by setting commit.gpgSign = true. And I have gpg-agent configured to use pinentry-curses to read my password since I normally commit from the terminal.

However, when I’ve tried to create a commit from magit, I was met with the following error:

gpg: signing failed: Inappropriate ioctl for device

After several failed attempts, I’m happy to report that I’ve finally figured out the issue and a solution.

What didn’t work

I initially followed the advice in this vxlabs blog post/reddit thread. It seemed promising, but it didn’t fix my issue.

I also found this reddit thread, which didn’t have a full solution either.

I also saw some references to pinentry.el, which is a package that allows all pinentry requests to be forwarded to emacs, which is not what I want. I still want to be able to sign commits from the terminal without going through emacs.

I also saw some suggestions to add allow-emacs-pinentry to gpg-agent.conf, but I didn’t find it necessary for my final solution. I think it’s probably relevant when using pinentry.el.

I also saw some suggestions to set the environment variable GPG_TTY=$(tty). I didn’t try this explicitly, but I don’t think it would have accomplished what I was after.

I also saw some suggestions just to use pinentry-gtk, which I didn’t want to do - I prefer using pinentry-curses from the terminal.

The problem

The vxlabs blog post described how to configure epa/epg to use the loopback pinentry mode to allow emacs to read the GPG password. It took quite a bit of reading through magit source code and tinkering with edebug to realize why their solution didn’t work for me. Finally, I realized that magit doesn’t actually use epg to sign the commit. It just shells out to the git binary, which in turn communicates with gpg-agent, so setting epg-pinentry-mode had no effect.

The solution

After I managed to get it working, I realized that magthe0 from the second reddit thread linked above was on the right track: just unlock gpg-agent from emacs right before commiting.

See, the suggestion in the vxlabs blog post does actually work. If I manually call epa-sign-file, it can read my password from a minibuffer correctly. And by doing so, gpg-agent will remember my password for a few minutes. So by automatically signing a string before commiting from magit, gpg-agent will already be happy when the git binary tries to authenticate. We can do this automatically via elisp’s advice-add

So my final solution looks like this:

Add the following to my emacs config:

;; pinentry config for gpg signing from within emacs
;; See https://vxlabs.com/2021/03/21/gnupg-pinentry-via-the-emacs-minibuffer/
(setq epg-pinentry-mode 'loopback)

(defun unlock-gpg-key (&rest _args)
  "Unlock the GPG key by attempting to sign something. Ignore all arguments."
  (let
      ((context (epg-make-context epa-protocol))
       (msg "test"))
    (epg-sign-string context msg)))

;; magit doesn't actually respect epg-pinentry-mode. It just calls the git binary,
;; which attempts to call pinentry-curses itself. This fails because it has no tty.
;; Therefore, we first attempt an epg signature to unlock the key from within emacs.
;; Then, gpg-agent will let magit sign the commit from the git binary :)
(advice-add 'magit-commit-create :before #'unlock-gpg-key)

And add the following to ~/.gnupg/gpg-agent.conf:

allow-loopback-pinentry

Happy committing :)

Caveats and follow-ups

This approach is not fool-proof — for example, it will not unlock the key if you are committing via a rebase or another path that does not go through magit-commit-create. You may still want to set GPG_TTY appropriately for terminal workflows.

Possible improvements:

See also