Below follows a Claude generated guide. It's not perfect and needs revision, I was able to partially follow it and get a working setup. I need to re-review this and document how this actually needs to work.
GPG Commit Signing in a Three-Hop Devcontainer Setup
The Problem
GPG agent forwarding was designed for a two-machine model:
You (laptop with keys + display) ──SSH──> Server (runs gpg, no keys)
The server forwards signing requests back to your laptop's agent. Your laptop
has a pinentry program (e.g., pinentry-mac) that pops up a dialog. You type
your passphrase. The signature is returned. Simple.
Our setup has three hops:
Mac (you sit here) ──VS Code SSH──> WSL/Linux (Docker host) ──devcontainer──> Container (you work here)
The VS Code Dev Containers extension runs on the Docker host (WSL). When it
sets up GPG forwarding, it runs gpgconf --list-dirs agent-extra-socket on
WSL and forwards that socket into the container. It has no awareness of the
Mac behind it.
The result:
- Container's
S.gpg-agent → forwarded to → WSL's S.gpg-agent.extra
- WSL is headless — no display, no TTY on the agent
- The extra socket runs in GnuPG's "restricted mode," which blocks loopback
pinentry (the workaround that would send the passphrase prompt back to your
terminal)
- Pinentry has nowhere to go. Signing fails.
The extension was designed for local Docker (two-layer: you + container) or
Codespaces (which bypasses GPG entirely by signing with GitHub's own key). A
remote Docker host with a developer on a third machine is an unsupported gap.
The Solution
Forward the Mac's GPG agent socket through all three hops so the container
ultimately talks to the Mac's agent, where pinentry-mac can prompt you:
Container gpg
→ socket forwarded by devcontainers extension →
WSL "extra socket" (actually Mac's agent, forwarded via SSH)
→ back to Mac's gpg-agent →
pinentry-mac pops up on your Mac screen
→ you type passphrase →
signature returned to container
Step-by-Step Setup
1. Mac: Install and configure GPG
Install GnuPG and pinentry-mac:
brew install gnupg pinentry-mac
If your GPG keys are currently on WSL, export them there and import on the Mac:
# On WSL — export
gpg --export-secret-keys --armor YOUR_KEY_ID > private-key.asc
gpg --export --armor YOUR_KEY_ID > public-key.asc
gpg --export-ownertrust > trust.txt
# On Mac — import
gpg --import private-key.asc
gpg --import public-key.asc
gpg --import-ownertrust trust.txt
# Clean up (don't leave private key files lying around)
Configure the Mac's gpg-agent (~/.gnupg/gpg-agent.conf):
pinentry-program /opt/homebrew/bin/pinentry-mac
default-cache-ttl 3600
max-cache-ttl 86400
Restart the agent:
gpgconf --kill gpg-agent
gpgconf --launch gpg-agent
Verify pinentry works locally:
echo "test" | gpg --clearsign
A macOS dialog should appear asking for your passphrase.
2. Mac: Note your socket paths
gpgconf --list-dirs agent-extra-socket
This will return something like:
/Users/yourname/.gnupg/S.gpg-agent.extra
Save this path — you'll need it in step 4.
3. WSL: Configure sshd to allow socket forwarding
On the WSL/Linux Docker host, edit /etc/ssh/sshd_config:
StreamLocalBindUnlink yes
This allows SSH to replace existing socket files when setting up remote
forwarding. Without this, the forwarding will fail if WSL's gpg-agent has
already created its own socket at that path.
Restart sshd:
sudo systemctl restart sshd
4. WSL: Note the extra socket path
gpgconf --list-dirs agent-extra-socket
This will return something like:
/home/damien/.gnupg/S.gpg-agent.extra
Save this path — you'll need it in step 5.
5. Mac: Configure SSH to forward GPG socket to WSL
Edit your SSH config on the Mac (~/.ssh/config):
Host your-wsl-host
HostName <ip-or-hostname>
User damien
RemoteForward /home/damien/.gnupg/S.gpg-agent.extra /Users/yourname/.gnupg/S.gpg-agent.extra
Replace the socket paths with the actual values from steps 2 and 4.
This tells SSH: when I connect to WSL, forward WSL's extra socket back to my
Mac's extra socket. The devcontainers extension will then forward this into the
container, completing the chain.
6. WSL: Stop the local gpg-agent
The SSH RemoteForward will replace the socket file (thanks to
StreamLocalBindUnlink), but WSL's gpg-agent may recreate it. Either stop it:
Or prevent it from starting by adding to ~/.bashrc or ~/.zshrc on WSL:
# Don't start gpg-agent on WSL — we forward from Mac
unset GPG_AGENT_INFO
Optionally, disable WSL's gpg-agent systemd unit if it exists:
systemctl --user disable gpg-agent.socket gpg-agent.service 2>/dev/null
7. WSL: Ensure the public keyring is available
The devcontainers extension copies pubring.kbx and trustdb.gpg from WSL
into the container. Since the keys now live on the Mac, WSL needs a copy of the
public keyring (not the private keys):
# On Mac — export public keyring
gpg --export --armor YOUR_KEY_ID > /tmp/public-key.asc
gpg --export-ownertrust > /tmp/trust.txt
# Copy to WSL (via scp or however you transfer files)
scp /tmp/public-key.asc /tmp/trust.txt your-wsl-host:~/
# On WSL — import
gpg --import ~/public-key.asc
gpg --import-ownertrust ~/trust.txt
rm ~/public-key.asc ~/trust.txt
8. Connect and test
-
SSH into WSL from your Mac (or open VS Code Remote-SSH to your WSL host).
The RemoteForward activates on connection.
-
Open your devcontainer.
-
In the container terminal:
# Verify the agent is reachable
gpg-connect-agent "getinfo version" /bye
# Test signing
echo "test" | gpg --clearsign
A pinentry-mac dialog should appear on your Mac. Type your passphrase.
- Test a commit:
git commit --allow-empty -S -m "test gpg signing"
Troubleshooting
"No agent running" or connection refused
The SSH RemoteForward may not be active. Check:
- Is your VS Code SSH connection using the right SSH config entry?
- Is
StreamLocalBindUnlink yes set in WSL's sshd_config?
- Is WSL's own gpg-agent recreating the socket? Kill it.
"Inappropriate ioctl for device"
The pinentry can't reach a display. Verify:
pinentry-mac is installed on the Mac: which pinentry-mac
- Mac's
gpg-agent.conf has pinentry-program /opt/homebrew/bin/pinentry-mac
- Mac's agent is running:
gpgconf --launch gpg-agent
"ERR 67109115 Forbidden" (restricted mode)
You're hitting the extra socket's restrictions. This is expected for admin
commands like updatestartuptty, but signing should still work through the
extra socket. If signing fails with this error, the socket forwarding may not
be connected to the Mac's agent.
Public key not found in container
The devcontainers extension copies the keyring from WSL. Make sure you imported
the public key on WSL (step 7). You can also verify inside the container:
Below follows a Claude generated guide. It's not perfect and needs revision, I was able to partially follow it and get a working setup. I need to re-review this and document how this actually needs to work.
GPG Commit Signing in a Three-Hop Devcontainer Setup
The Problem
GPG agent forwarding was designed for a two-machine model:
The server forwards signing requests back to your laptop's agent. Your laptop
has a pinentry program (e.g.,
pinentry-mac) that pops up a dialog. You typeyour passphrase. The signature is returned. Simple.
Our setup has three hops:
The VS Code Dev Containers extension runs on the Docker host (WSL). When it
sets up GPG forwarding, it runs
gpgconf --list-dirs agent-extra-socketonWSL and forwards that socket into the container. It has no awareness of the
Mac behind it.
The result:
S.gpg-agent→ forwarded to → WSL'sS.gpg-agent.extrapinentry (the workaround that would send the passphrase prompt back to your
terminal)
The extension was designed for local Docker (two-layer: you + container) or
Codespaces (which bypasses GPG entirely by signing with GitHub's own key). A
remote Docker host with a developer on a third machine is an unsupported gap.
The Solution
Forward the Mac's GPG agent socket through all three hops so the container
ultimately talks to the Mac's agent, where
pinentry-maccan prompt you:Step-by-Step Setup
1. Mac: Install and configure GPG
Install GnuPG and pinentry-mac:
If your GPG keys are currently on WSL, export them there and import on the Mac:
Configure the Mac's gpg-agent (
~/.gnupg/gpg-agent.conf):Restart the agent:
Verify pinentry works locally:
A macOS dialog should appear asking for your passphrase.
2. Mac: Note your socket paths
This will return something like:
Save this path — you'll need it in step 4.
3. WSL: Configure sshd to allow socket forwarding
On the WSL/Linux Docker host, edit
/etc/ssh/sshd_config:This allows SSH to replace existing socket files when setting up remote
forwarding. Without this, the forwarding will fail if WSL's gpg-agent has
already created its own socket at that path.
Restart sshd:
4. WSL: Note the extra socket path
This will return something like:
Save this path — you'll need it in step 5.
5. Mac: Configure SSH to forward GPG socket to WSL
Edit your SSH config on the Mac (
~/.ssh/config):Replace the socket paths with the actual values from steps 2 and 4.
This tells SSH: when I connect to WSL, forward WSL's extra socket back to my
Mac's extra socket. The devcontainers extension will then forward this into the
container, completing the chain.
6. WSL: Stop the local gpg-agent
The SSH
RemoteForwardwill replace the socket file (thanks toStreamLocalBindUnlink), but WSL's gpg-agent may recreate it. Either stop it:Or prevent it from starting by adding to
~/.bashrcor~/.zshrcon WSL:Optionally, disable WSL's gpg-agent systemd unit if it exists:
systemctl --user disable gpg-agent.socket gpg-agent.service 2>/dev/null7. WSL: Ensure the public keyring is available
The devcontainers extension copies
pubring.kbxandtrustdb.gpgfrom WSLinto the container. Since the keys now live on the Mac, WSL needs a copy of the
public keyring (not the private keys):
8. Connect and test
SSH into WSL from your Mac (or open VS Code Remote-SSH to your WSL host).
The
RemoteForwardactivates on connection.Open your devcontainer.
In the container terminal:
A pinentry-mac dialog should appear on your Mac. Type your passphrase.
git commit --allow-empty -S -m "test gpg signing"Troubleshooting
"No agent running" or connection refused
The SSH RemoteForward may not be active. Check:
StreamLocalBindUnlink yesset in WSL's sshd_config?"Inappropriate ioctl for device"
The pinentry can't reach a display. Verify:
pinentry-macis installed on the Mac:which pinentry-macgpg-agent.confhaspinentry-program /opt/homebrew/bin/pinentry-macgpgconf --launch gpg-agent"ERR 67109115 Forbidden" (restricted mode)
You're hitting the extra socket's restrictions. This is expected for admin
commands like
updatestartuptty, but signing should still work through theextra socket. If signing fails with this error, the socket forwarding may not
be connected to the Mac's agent.
Public key not found in container
The devcontainers extension copies the keyring from WSL. Make sure you imported
the public key on WSL (step 7). You can also verify inside the container: