Learning how to use the ssh-agent with fish
tl;dr: Use eval (ssh-agent -c)
instead of eval $(ssh-agent -s)
, or use fish-ssh-agent instead of the oh-my-zsh ssh-plugin.
Up until my switch from zsh to fish I used this oh-my-zsh plugin to start the ssh-agent
for me. That just worked, which meant that I never learned how the ssh-agent
works in any detail. Switching to fish however, I had to play around with the ssh-agent
in order to set it up correctly. In this short post we’ll go through what I learned.
Let’s start by taking a closer look at the oh-my-zsh plugin. We can see that it’s using the ssh-agent -s
command. If I execute that in a bash shell or a z shell, this is what I see:
> ssh-agent -s
SSH_AUTH_SOCK=/tmp/ssh-ZKlr8on2tPAf/agent.155283; export SSH_AUTH_SOCK;
SSH_AGENT_PID=155284; export SSH_AGENT_PID;
echo Agent pid 155284;
You’ll recognize the output as bash/zsh commands. There are five of them:
SSH_AUTH_SOCK=/tmp/ssh-ZKlr8on2tPAf/agent.155283
export SSH_AUTH_SOCK
SSH_AGENT_PID=155284
export SSH_AGENT_PID;
echo Agent pid 155284;
What you would usually do is to execute these commands doing something like eval $(ssh-agent -s)
. That would’ve printed “Agent pid 155284”, and you would get some environment variables available:
> eval $(ssh-agent -s)
Agent pid 155284
> echo $SSH_AUTH_SOCK
/tmp/ssh-ZKlr8on2tPAf/agent.155283
> echo $SSH_AGENT_PID
155284
This would effectively make it possible for other processes to find the running ssh-agent
, by looking at those environment variables.
So, what happens if we attempt the same using fish?
> eval $(ssh-agent -s)
fish: $(...) is not supported. In fish, please use '(ssh-agent)'.
It didn’t work, but fish guides us to try something else (drop the “$”):
> eval (ssh-agent -s)
fish: Unsupported use of '='. In fish, please use 'set SSH_AUTH_SOCK /tmp/ssh-LlFHoPiRmpD9/agent.155283'.
Well, that didn’t work either. And the guiding is now a little harder to understand. Recall what running ssh-agent -s
outputs:
> ssh-agent -s
SSH_AUTH_SOCK=/tmp/ssh-ZKlr8on2tPAf/agent.155283; export SSH_AUTH_SOCK;
SSH_AGENT_PID=155284; export SSH_AGENT_PID;
echo Agent pid 155284;
It’s those five bash/zsh commands we saw earlier:
SSH_AUTH_SOCK=/tmp/ssh-ZKlr8on2tPAf/agent.155283
export SSH_AUTH_SOCK
SSH_AGENT_PID=155284
export SSH_AGENT_PID;
echo Agent pid 155284;
What happens if we execute one of these bash/zsh commands in fish?
> SSH_AUTH_SOCK=/tmp/ssh-ZKlr8on2tPAf/agent.155283
fish: Unsupported use of '='. In fish, please use 'set SSH_AUTH_SOCK /tmp/ssh-ZKlr8on2tPAf/agent.155283'.
Now the tip that fish gives us makes a little more sense. Let’s try following it:
> set SSH_AUTH_SOCK /tmp/ssh-ZKlr8on2tPAf/agent.155283;
> echo $SSH_AUTH_SOCK
/tmp/ssh-ZKlr8on2tPAf/agent.155283
That worked! So that’s why eval (ssh-agent -s)
didn’t do the trick for us: The syntax of the output does not work in fish. Luckily for us, there’s an alternative that does give us a syntax that works:
> eval (ssh-agent -c)
Agent pid 157700
If we now try to echo our variables, we can see that they have values:
> echo $SSH_AUTH_SOCK
/tmp/ssh-MqoPu4YHHjm4/agent.157699
> echo $SSH_AGENT_PID
157700
So whenever you’re trying to add an SSH key and you get this error:
> ssh-add ~/.ssh/id_my_key
Error connecting to agent: No such file or directory
Then you should be able to run eval (ssh-agent -c)
to 1) start the ssh-agent
and 2) set some environment variables that makes it possible for other processes to find the agent.
But there’s a catch: What if you open a new shell? Those environment variables won’t be available there. If you run eval (ssh-agent -c)
in a new shell, that will start a second ssh-agent
. Let’s see it happen:
> killall ssh-agent
> eval (ssh-agent -c)
Agent pid 161863
> ps x | grep ssh-agent
2187 ? Zs 0:00 [ssh-agent] <defunct>
161863 pts/3 S+ 0:00 grep --color=auto ssh-agent
> eval (ssh-agent -c)
Agent pid 161995
> ps x | grep ssh-agent
2187 ? Zs 0:00 [ssh-agent] <defunct>
161863 ? Ss 0:00 ssh-agent -c
161995 ? Ss 0:00 ssh-agent -c
162042 pts/3 S+ 0:00 grep --color=auto ssh-agent
We did a few things there:
- Kill all running
ssh-agent
's usingkillall
, just to clean up our previously started agents. - Start a new ssh-agent using
eval (ssh-agent -c)
. - Check the running processes looking for ssh-agents using
ps
andgrep
. - Start another ssh-agent using
eval (ssh-agent -c)
. - Check again the running processes looking for ssh-agents using
ps
andgrep
.
At step 3 there we only saw one running ssh-agent
, but at step 5 we saw two of them. That’s not what we want to happen!
If we take yet another look at the oh-my-zsh plugin we can see this:
ssh-agent -s ${lifetime:+-t} ${lifetime} | sed 's/^echo/#echo/' >! $_ssh_env_cache
What’s happening here is that we’re caching the output of the ssh-agent -s
command into a file (a filename stored in the $_ssh_env_cache
variable). This is what the plugin does for us whenever we open a new Z shell:
- Check if we have a file
$_ssh_env_cache
. If we do, then execute it and go to step 2. If we don’t then jump to step 3. - Check if there’s a running
ssh-agent
(with the pid that we found in the$_ssh_env_cache
file). If there is, then we stop here. If there isn’t, go to step 3. - If no cache file was found, or if no ssh-agent was found on the pid from the cache file, then run
eval $(ssh-agent -s)
and cache the results in the file$_ssh_env_cache
.
This makes sure that we’re not starting multiple ssh-agent
's unnecessarily. We could create a fish script that does it for us, but luckily one is already available: fish-ssh-agent does this for us.