Troubleshooting “gpg failed to sign the data” Jump to heading

error: gpg failed to sign the data
fatal: failed to write commit object

That message is Git reporting that the signing program exited non-zero. It says nothing about why, because Git does not know. The real error is printed by gpg (or ssh-keygen) and is usually swallowed. There are perhaps eight common causes, and guessing between them wastes an afternoon. This page gives a fixed diagnostic order: make the real error visible first, then check each cause from most to least likely. It sits within the GPG vs SSH commit signing topic, and most of it applies to SSH signing too.

When to use this approach Jump to heading

  • git commit fails with “gpg failed to sign the data” or “failed to write commit object”.
  • Signing worked yesterday and stopped without any configuration change.
  • Signing works in a terminal but fails in an editor, a GUI client, or a hook.
  • Signing fails only in CI or a container — in that case also see signing commits inside ephemeral containers.

Step 1 — Make the real error visible Jump to heading

Run the signing program the way Git does, outside Git. That prints the error Git hides.

# What Git will call, and with which key
git config --get gpg.program || echo gpg
git config --get user.signingKey

# Sign something by hand with the same key
echo test | gpg --clearsign --local-user "$(git config user.signingKey)"

For more detail from Git itself, trace the call:

GIT_TRACE=1 git commit --allow-empty -m "trace" 2>&1 | grep -iE 'gpg|run_command'

The trace shows the exact command line Git runs. Copy it and run it by hand; whatever it prints is the real diagnosis.

Read the real error firstRunning the signing program by hand turns one vague Git message into a specific one. Inappropriate ioctl or no pinentry points at the terminal, no secret key or unusable key points at the key ID or expiry, and connection errors point at the agent.What does gpg print when run by hand?Inappropriate ioctlTerminalGPG_TTY or pinentryNo secret key / unusableKeyID, expiry, subkeycan't connect to agentAgentrestart, socketthree families of message cover almost every case

Step 2 — Fix the terminal: GPG_TTY and pinentry Jump to heading

The most frequent cause on Linux and macOS. The agent needs to show a passphrase prompt and does not know which terminal to use.

export GPG_TTY=$(tty)
echo 'export GPG_TTY=$(tty)' >> ~/.bashrc     # or ~/.zshrc
gpg-connect-agent updatestartuptty /bye

In editors and GUI clients there is no terminal at all, so a terminal pinentry cannot work. Configure a graphical one.

# ~/.gnupg/gpg-agent.conf — pick the one installed on your system
pinentry-program /usr/bin/pinentry-gnome3
# macOS with pinentry-mac:
# pinentry-program /opt/homebrew/bin/pinentry-mac
gpgconf --kill gpg-agent      # agent restarts on next use with the new setting
echo test | gpg --clearsign >/dev/null && echo "signing ok"

Step 3 — Check the key ID and that it can sign Jump to heading

A key ID that does not exist, or names a key without the sign capability, fails immediately. So does a key whose secret part is not on this machine — common after following keeping a GPG primary key offline and pointing Git at the primary instead of the subkey.

gpg --list-secret-keys --keyid-format long
# sec#  ed25519/0xA1B2... [C]             <- primary, secret absent (#)
# ssb   ed25519/0x1122... [S] [expires…]  <- signing subkey, present

git config --global user.signingKey 0x1122334455667788!   # the [S] subkey, with !

The trailing ! forces that exact subkey. Without it, GPG may choose another key from the same certificate that it cannot use.

Step 4 — Check expiry Jump to heading

An expired signing subkey produces “unusable secret key” or simply fails. It happens exactly on the anniversary you set a year ago, which is why it feels like it broke for no reason.

gpg --list-keys --with-colons "$(git config user.signingKey | tr -d '!')" |
  awk -F: '$1 ~ /pub|sub/ {print $1, $5, "expires:", ($7 ? strftime("%F", $7) : "never")}'

If the subkey is expired, extend it — which needs the primary — or add a new subkey. Extending is quicker and keeps the same key ID:

# With the primary key available (offline machine or temporary GNUPGHOME)
gpg --quick-set-expire "$PRIMARY_FPR" 1y "$SUBKEY_FPR"
gpg --export --armor "$PRIMARY_FPR" > pub.asc     # re-upload to the forge
How often each cause turned out to be the answerAn illustrative split of causes as they typically show up in support requests: a missing terminal or pinentry is the largest share, followed by wrong key IDs and expired subkeys, with agent problems and wrong program paths making up the remainder.typical share of incidents by root cause (illustrative)no TTY / pinentry41%wrong key ID22%expired subkey17%stuck agent12%wrong gpg.program8%check in this order and most cases are solved by the second step

Step 5 — Restart a stuck agent Jump to heading

After sleep, a socket change, or an upgrade, the agent can hang or point at a stale socket. Restarting it is cheap and safe.

gpgconf --kill gpg-agent
gpg-connect-agent /bye           # starts a fresh agent
gpgconf --list-dirs agent-socket # the socket every client should use

If you use the agent for SSH authentication as well, set SSH_AUTH_SOCK from gpgconf --list-dirs agent-ssh-socket so the two do not disagree.

Where the chain breaks when the agent is staleGit calls gpg, which tries to reach the agent through the socket path it expects. After a sleep or upgrade the socket can point at an agent that no longer answers, so gpg times out and Git reports only that signing failed. Killing the agent lets the next call start a fresh one.gitgpgagent socketsign this commitconnectno answer / timeoutexit 2gpg failed to signgpgconf --kill gpg-agent clears the stale state; the next request starts a new agent

Step 6 — Check gpg.program and the environment Git runs in Jump to heading

If you set gpg.program to gpg2, a Homebrew path, or a Windows path, and that binary moved, Git fails with the same message. Hooks and GUI clients also run with a different PATH and HOME, so a bare gpg can resolve to a different binary with a different keyring.

git config --show-origin --get gpg.program
command -v "$(git config --get gpg.program || echo gpg)"
# Use an absolute path to remove ambiguity
git config --global gpg.program "$(command -v gpg)"

For SSH signing, the equivalent setting is gpg.ssh.program, and the equivalent failure is an ssh-keygen that predates -Y sign support.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Why does signing work in my terminal but not in the editor? Jump to heading

The editor starts Git without your shell’s environment: no GPG_TTY, sometimes a different PATH. A graphical pinentry and an absolute gpg.program remove both dependencies. On Windows with WSL there may also be a different Git involved; see signing commits on Windows and WSL.

Can I just turn off signing to get the commit through? Jump to heading

git commit --no-gpg-sign creates an unsigned commit, which a signature gate will reject later. It is a reasonable way to save work in an emergency, but re-sign before pushing: git commit --amend --no-edit -S once signing works.

Does this error ever mean the commit content is the problem? Jump to heading

No. The commit object is just bytes to the signing program. The cause is always the program, the key, the agent or the environment.