How to setup a clipboard over ssh

This article explains how to get a working clipboard integration to the local machine when connecting over ssh.

The problem

When connecting over ssh, the remote host can’t post updates to the local clipboard, which makes transferring a piece of text from one connected machine to the other a pain of using the mouse and if tmux or such is being involved, then even worse.

The solution

Having a fifo file on the remote host through which we can yank whatever text we’d like to the local host, by also listening to the fifo from it using an additional connection.

It doesn’t require anything from the remote machine - no installation, no special packages.

TL;DR

Paste the following into your bashrc and use sshx host to connect.

On the remote machine echo SOMETHING > ~/clip and hopefully, SOMETHING will end up in the local host’s clipboard.

You will need the xclip utility on your local host.

_dt_term_socket_ssh() {
	ssh -oControlPath=$1 -O exit DUMMY_HOST
}
function sshx {
	local t=$(mktemp -u --tmpdir ssh.sock.XXXXXXXXXX)
	local f="~/clip"
	ssh -f -oControlMaster=yes -oControlPath=$t $@ tail\ -f\ /dev/null || return 1
	ssh -S$t DUMMY_HOST "bash -c 'if ! [ -p $f ]; then mkfifo $f; fi'" \
		|| { _dt_term_socket_ssh $t; return 1; }
	(
	set -e
	set -o pipefail
	while [ 1 ]; do
		ssh -S$t -tt DUMMY_HOST "cat $f" 2>/dev/null | xclip -selection clipboard
	done &
	)
	ssh -S$t DUMMY_HOST \
		|| { _dt_term_socket_ssh $t; return 1; }
	ssh -S$t DUMMY_HOST "command rm $f"
	_dt_term_socket_ssh $t
}

This solution is relying heavily on an ssh feature called ControlMaster. In short, this feature allows to piggy back several ssh connections on a single actual tcp one. Here we use it to open an additional connection that waits on a fifo file for clipboard content.

Let’s break it down.

	local t=$(mktemp -u --tmpdir ssh.sock.XXXXXXXXXX)

this generates a temporary file name in the ssh.sock.* pattern in the /tmp directory (probably, depends on your system).

	local f="~/clip"

is the remote path for the fifo file representing the clipboard.

	ssh -f -oControlMaster=yes -oControlPath=$t $@ tail\ -f\ /dev/null || return 1

create a new master connection and hold it with the tail -f /dev/null command. This will also create the socket file through which we’ll make the additional connections.

	ssh -S$t DUMMY_HOST "bash -c 'if ! [ -p $f ]; then mkfifo $f; fi'" \
		|| { _dt_term_socket_ssh $t; return 1; }

use the existing socket to create the fifo file. It will fail if it already exists but is not a fifo file - and that is on purpose. We’re ssh’ing to DUMMY_HOST because when a socket is specified with the -S parameter, it doesn’t matter which host you mention on the command line. If there is a failure, we’ll call the _dt_term_socket_ssh function:

_dt_term_socket_ssh() {
	ssh -oControlPath=$1 -O exit DUMMY_HOST
}

and this is an important one: when we call the socket with -O exit, it terminates all of the connections going through it.

	(
	set -e
	set -o pipefail
	while [ 1 ]; do
		ssh -S$t -tt DUMMY_HOST "cat $f" 2>/dev/null | xclip -selection clipboard
	done &
	)

this subshell is the clipboard listener. Thanks to the set -e, when the exit call is received, the ssh process terminates with exit code 255 and terminates the loop and the subshell.

	ssh -S$t DUMMY_HOST \
		|| { _dt_term_socket_ssh $t; return 1; }

this is the actual session. When you logout and this blocking command terminates this will be the time to cleanup:

	ssh -S$t DUMMY_HOST "command rm $f"
	_dt_term_socket_ssh $t

After this, nothing is left - no fifo, no listener and no socket file on local machine.

A bonus snippet - a vim clipboard writer for the remote host:

function! s:DumpContToFile(reg_cont, fname)
	if !filereadable(a:fname)
		echoerr a:fname . " does not exist"
		return
	endif
	execute "redir! > " . a:fname
	silent echon a:reg_cont
	redir END
	echo "Dumped to remote clipboard"
endfunction
nnoremap <silent> <Space>c :call <SID>DumpContToFile(@", expand("~")."/clip")<CR>

Will basically write the default register to ~/clip bound to <Space>c

And that’s it. Tell me what you think in a mail or something :)


Notes:

  • If you’re getting an error like mux_master_process_new_session: tcgetattr: Inappropriate ioctl for device, consider applying this patch. There is nothing inherently wrong, this message just needs to be filtered.