This is the last post in this series, at least for now. I started last week with the idea of reading through man ssh and man ssh_config daily and writing about what I found.
Eight posts later, I've covered ObscureKeystrokeTiming, ChannelTimeout, Match version,
Match sessiontype, Escape Sequences, AddKeysToAgent, and ControlMaster.
I'm pausing the daily posting, between my physics analysis work and GPU work, daily posts aren't realistic right now. I'll probably pick this back up later, there's no shortage of material in these man pages.
So let's move to today's final entry to the list, LocalCommand and I will make it quicker. So lets begin with what the man page say:
From the man page:
LocalCommand
Specifies a command to execute on the local machine after
successfully connecting to the server. The command string
extends to the end of the line, and is executed with the
user's shell. Arguments to LocalCommand accept the tokens
described in the TOKENS section.
The command is run synchronously and does not have access
to the session of the ssh(1) that spawned it. It should
not be used for interactive commands.
This directive is ignored unless PermitLocalCommand has
been enabled.
Two things makes this interesting. First, it's a post-connection hook, it fires on our local machine after ssh has fully authenticated with the remote server.
Second, it has access to all the tokens: %h (remote hostname), %r (remote user), %p (remote port), %n (hostname as typed on the command line), %d (local home directory), %l (local hostname),%u (local username), and more. This gives us a programmable callback that knows the full context of the connection that just happened.
To use it, we need to explicitly enable it:
Host *
PermitLocalCommand yes
Without PermitLocalCommand yes, all LocalCommand directives are silently ignored. This is off by default for good reason, we don't want arbitrary commands running as a side effect of ssh-ing somewhere,
especially if system-wide ssh_config could be modified by an admin.
The simplest use is logging. Say we want to keep a record of every SSH connection we make:
Host *
PermitLocalCommand yes
LocalCommand echo "$(date '+%Y-%m-%d %H:%M:%S') %u@%l -> %r@%h:%p" >> ~/.ssh/connection.log
Every time we connect, a line gets appended to ~/.ssh/connection.log with a timestamp, who we are, and where we went.
Useful for auditing our own activity or debugging "when did I last connect to that machine."
We can use it for notifications. i.e, On macOS:
Host production-*
PermitLocalCommand yes
LocalCommand osascript -e 'display notification "Connected to %h as %r" with title "SSH"'
Every time we connect to a production host, we get a system notification. A small thing, but it adds a moment of awareness when we're about to do something on a live system.
A more practical pattern: syncing something after connecting. If we keep dotfiles or a specific config on remote hosts:
Host dev-*
PermitLocalCommand yes
LocalCommand rsync -q ~/.vimrc %r@%h:.vimrc
This pushes our local .vimrc to the remote host every time we connect. The command runs synchronously before we get our shell prompt, so by the time we're typing on the remote machine, the file is already there.
Keep in mind this adds latency to our connection, a quick rsync is fine, anything heavy is not.
There are important limitations. The command runs synchronously and blocks the connection until it completes. If it hangs, our ssh session hangs. If it fails, we still get connected,LocalCommand failures don't abort the SSH connection. The command doesn't have access to the SSH session itself: it can't read from or write to the remote shell.
It's a fire-and-forget local action that happens to know about the connection.
Also, LocalCommand only fires for interactive sessions by default. If we run ssh host ls /tmp, or use scp or sftp, the command won't execute.
We can combine this with Match sessiontype from earlier in this series if we want it to fire only for specific session types, or avoid firing for others.
We can scope it per host as we'd expect:
Host *.cern.ch
PermitLocalCommand yes
LocalCommand echo "$(date '+%Y-%m-%d %H:%M:%S') -> %r@%h" >> ~/.ssh/cern.log
Host bastion
PermitLocalCommand yes
LocalCommand echo "Jumped through bastion" >> ~/.ssh/connection.log
The PermitLocalCommand + LocalCommand pair is one of those features that's been in ssh_config for a long time but barely anyone uses. It's not revolutionary on its own, but it's a building block.
The fact that it has access to all the connection tokens means we can wire it up to whatever local tooling makes sense for our workflow, logging, notifications, syncing, triggering scripts, updating a status file.
It's the closest thing ssh_config has to a plugin system.
That's it for this series. There are topics I didn't get to, CanonicalizeHostname, UpdateHostKeys, KnownHostsCommand, RemoteCommand, VisualHostKey, the full TOKENS section, SSH certificates, and plenty more.
The man pages are dense with things worth knowing if we can push past just looking up flags.
The whole point of writing these was to force myself to actually read the pages, and it worked,
I've already changed my own ssh_config based on what I found. Maybe that's enough of a reason to pick it back up when things quiet down.