Remote clipboard interactions

2025.Jan.22

One common task I run that does not have an obvious “good” solution when working with remote machines over ssh is: how one can copy information from a remote ssh session to your local? I’m not talking about copying files down from the remote machine via scp command, I’m talking like getting the path string from the remote session via commands like ls/pwd so that you can properly form the scp command; or cases such as getting outputs form command line dumps to be used in a local configurations file. While there is an “obvious” solution of using your mouse to highlight the string of interest, and copying into you buffer through your terminal emulator, this solution is rather unsatisfactory for following reasons:

  • Inaccuracies of the mouse operations: while there are common interactions such as “double-click to highlight words”, “triple-click to highlight lines”, the definition of “words” and “lines” depends on quite of bit of your settings, and is not immutable. Things like are you working with in tmux or other embedded terminal session will change this what the definition of “lines” are as determined by the most front facing software that he determined what is to be copied. The issue is currently further exacerbated by if you have tmux split panes with one pane having a rolling output, where the highlight may or may not shift with the activity of the other pane.
  • Lossy history. Because what one does with the mouse is not being recorded, getting into the habit of using the mouse means that the certain actions are lost in the total even history, especially if you get into the habit of the copying “partial” outputs rather than the entire command output.

While neither of these things are really “game breaking” in terms of terminal workflow, I wanted to check if there is something nicer that encourages me to take full use of the command line tools. So, the general question that needs to be answered would be: is there a way to stream data to a separate non-output processes over the ssh session? And the answer is a resounding: yes!

SSH reverse proxy

The first ingredient to solving the problem is ssh reverse proxy (or more formally “remote forwarding”), this can be invoked either interactively with the command:

[bash]
ssh -R 9123:localhost:9123 user@host

Or it can be set permanently in your ~/.ssh/config file with something like:

[text]
Host host
    User user
    RemoteForward 9123 localhost:9123

Users of running jupyter on clusters are likely more familiar with the counterpart of ssh port forwarding (with the -L 9122:localhost:9122 flag), and the functionalities basically mirror each other. In the case port forwarding, all network traffic to the address/port pair of localhost:9122 on the local machine will be passed to the remote machine’s localhost:9122, where it can be processed by arbitrary programs listening in on port 9122 on the remote machine, this is how this mechanism can be used to have you interact with the remote machines jupyter server by using the browser pointing to a localhost address. The reverse proxy basically reverses this traffic flow logic: for any traffic that attempts to pass to the localhost:9123 on the remote machine, this traffic is now re-routed to localhost:9123 on the local machine to be processed.

This, of course, requires you to set up some program on your local machine to handle this traffic, here we can write a very dumb program to handle to basically spit out what is being handled on passed to the traffic via a python program:

basic_handler.py [python]
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 9123))
s.listen(1)
while True:
     connection, address = s.accept()
     print(connection.recv(65536))

If you run this simple snippet on your local machine, start an ssh connection with the -R flag listed previously, then run the following command on the remote machine:

[bash]
echo "MYTEST" | nc localhost 9123

You can see the information from the command being passed to the python session running on the local machine! This immediately opens up the possibility any information parsing! In general, because our target the system clipboard on our local machine, we should try to have some form protection to ensure that only valid traffic and end up in the clipboard. So instead of the slightly unwieldy nc, we will be righting a companion script to emit traffic from the remote machine:

emitter.py [python]
import socket
import sys
import json
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("localhost", int(os.getenv("RCB_PORT", "9543"))))
    s.send(json.dumps({"token": "super_secret_token", "msg": sys.stdin.read().rstrip()}).encode("utf8"))

This makes it so that this script interacts with the basic nc command shown above (so you should use this script using the syntax like: my_command | python emitter.py), except we wrap the command output into a simple JSON string with the addition of a secret token. On the listener side, we can also add in a quick validation script to only process request with a valid token:

listener_parital.py [python]
import socket
import json

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 9123))
s.listen(1)
while True:
     connection, address = s.accept()
     request = json.loads(connection.recv(65536).decode("utf8"))
     if request["token"] in valid_tokens: # Only process valid tokens
         print(request["msg"])

Now we have one additional ingredient left, that that is how to have this listner.py script work with the system clipboard!

Command-line clipboard interaction

The reason why I used the command line pipe is that this actually mirrors what you want to do for clipboard interaction in a local command line session! The common methods for this would be something like:

[bash]
mycommand | xclipboard # For Linux X11 session
mycommand | wl-copy    # For Linux wayland sessions
mycommand | pbcopy     # For macOS session

We basically need to emulate this behavior in our listener script! This can be done very simply with the python subprocess module:

listener.py [python]
import socket
import json
import subprocess

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 9123))
s.listen(1)
while True:
     connection, address = s.accept()
     request = json.loads(connection.recv(65536).decode("utf8"))
     if request["token"] in valid_tokens: # Only process valid tokens
         cb_process = subprocess.Popen("wl-copy", shell=True, stdin=subprocess.PIPE)
         cb_process.stdin.write(request["msg"].encode("utf8"))
         cb_process.stdin.close()
         cb_process.wait()

And we are done! The only remaining things would be:

  • Make sure the emitter.py scripts is located on a remote that is accessible via the $PATH variable.
  • Make a way to have the listener.py script automatically start up (ex: setting it up as a user-level daemon)
  • Setting up your SSH configurations to use the correct port for reverse proxy. (Notice that multiple remote hosts can use a common port!)

This has potentially security implementation, I will not be discussion how this is done here (also if you are implementing this yourself, you should also implement a nicer method for handling the secret validation token), then you should be free to simply to use my_command | emitter.py where ever you go and get the result into your local clipboard to be either included in presentation or paper! My own implementation of this can be found in my dotfiles repository here and here.