TL;DR

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# ~/.config/systemd/user/ssh-agent.service
[Unit]
Description=SSH authentication agent

[Service]
ExecStart=/usr/bin/ssh-agent -a %t/ssh-agent.socket -D
Type=simple

[Install]
WantedBy=default.target
1
2
systemctl --user daemon-reload
systemctl --user enable --now ssh-agent

And in your ~/.bashrc or ~/.zshrc:

1
2
3
4
5
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"
if ! ssh-add -l &>/dev/null; then
  echo "Adding keys..."
  ssh-add -t 1d ~/.ssh/id_ed25519
fi

Log out and back in. Every shell now shares one agent on a stable socket, and you never spawn a fresh ssh-agent per terminal.

The classic pattern (eval "$(ssh-agent)" in .bashrc) starts one agent per shell, leaks orphaned processes, and loses keys between terminals. Letting systemd own the agent fixes all three: one process, one socket, one lifecycle.

Requirements

openssh (ships ssh-agent and ssh-add) and a systemd user session (any modern distro with logind). Check:

1
ssh-agent -V 2>/dev/null; ssh-add -l; echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"

If $XDG_RUNTIME_DIR is empty you don’t have a real user session (e.g. you got here via su); log in over SSH or on the console as your own user.

1. Create the user service

A --user service lives in ~/.config/systemd/user/ and runs as your account, no root:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/ssh-agent.service <<'EOF'
[Unit]
Description=SSH authentication agent

[Service]
ExecStart=/usr/bin/ssh-agent -a %t/ssh-agent.socket -D
Type=simple

[Install]
WantedBy=default.target
EOF
  • %t expands to the user runtime dir ($XDG_RUNTIME_DIR, usually /run/user/<uid>), so the socket lands at /run/user/<uid>/ssh-agent.socket.
  • -a pins that socket path (instead of a random one under /tmp), which is what we export next.
  • -D keeps the agent in the foreground; that’s correct for Type=simple.

If you’d rather have it apply to all users on the box, drop the file in /etc/systemd/user/ssh-agent.service (needs root). For your own account, ~/.config/systemd/user/ is the clean choice. Pick one location, not both.

2. Export the socket and load the key on login

Add this to your ~/.bashrc or ~/.zshrc:

1
2
3
4
5
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"
if ! ssh-add -l &>/dev/null; then
  echo "Adding keys..."
  ssh-add -t 1d ~/.ssh/id_ed25519
fi
  • The export must come before the ssh-add, or the client won’t know which agent to talk to.
  • ssh-add -l exits 1 when the agent has no keys and 2 when it can’t reach the socket; the if ! covers both and (re)loads.
  • -t 1d makes the key expire after a day. Drop -t if you want it to live until you restart the agent.

3. Enable and verify

1
2
3
systemctl --user daemon-reload
systemctl --user enable --now ssh-agent
systemctl --user status ssh-agent   # active (running)

Open a new terminal (or source ~/.bashrc) and confirm the key is loaded:

1
ssh-add -L   # lists the public keys held by the agent

What each piece does

PieceWhat it does
ssh-agent.service (--user)Keeps one agent alive, owned by systemd
-a %t/ssh-agent.socketSocket at a fixed, predictable path
SSH_AUTH_SOCK in the rcPoints every shell at that socket
ssh-add -t 1d id_ed25519Loads the key on login, with optional expiry

Traps

  • Use your key’s real name. Many guides say id_rsa; modern keys are id_ed25519. If the file doesn’t exist, ssh-add fails quietly inside the if.
  • Lingering: by default the user service dies when you close your last session. If you need the agent alive without an active login (cron, a CI runner on the box), enable lingering: loginctl enable-linger $USER.
  • The socket must match. The unit’s -a %t/ssh-agent.socket and the rc’s SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket" point at the same file. Change one, change the other.
  • &>/dev/null is bash/zsh. Under a strict /bin/sh (dash) use >/dev/null 2>&1.
  • Passphrase: if your key has a passphrase, the login ssh-add prompts once per expiry. With -t 1d, once a day.
  • Don’t mix agents. If a profile still does eval "$(ssh-agent)", remove it; otherwise you end up with two agents and a SSH_AUTH_SOCK that flips between them.

See also