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:

  1. SSH_AUTH_SOCK=/tmp/ssh-ZKlr8on2tPAf/agent.155283
  2. export SSH_AUTH_SOCK
  3. SSH_AGENT_PID=155284
  4. export SSH_AGENT_PID;
  5. 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:

  1. SSH_AUTH_SOCK=/tmp/ssh-ZKlr8on2tPAf/agent.155283
  2. export SSH_AUTH_SOCK
  3. SSH_AGENT_PID=155284
  4. export SSH_AGENT_PID;
  5. 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:

  1. Kill all running ssh-agent's using killall, just to clean up our previously started agents.
  2. Start a new ssh-agent using eval (ssh-agent -c).
  3. Check the running processes looking for ssh-agents using ps and grep.
  4. Start another ssh-agent using eval (ssh-agent -c).
  5. Check again the running processes looking for ssh-agents using ps and grep.

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:

  1. 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.
  2. 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.
  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.