For quite a while now, I’ve done a large amount of my text editing in emacs
over ssh using TRAMP. It’s extremely convenient to be able to run one
instance of emacs locally instead of spawning an editor remotely and
interacting with it over ssh. I’ve tried using sshfs in the past, but it’s
never worked well for me in the general case. But, this isn’t a post about
justifying my choices.
The other crucial part of my local emacs setup has been emacsclient. Setting
my local $EDITOR to emacsclient means that I could, for example, run git
commit in a terminal and edit the commit message without having to spawn a
new emacs instance. Instead, emacsclient tells the existing emacs what files
to edit, and waits until emacs says it’s done with them.
Until just yesterday, there was a major flaw in my setup: I couldn’t activate
my local emacs instance through emacsclient on a remote host. This meant that
$EDITOR had to be set to something that spawned an editor on the remote host,
which was becoming increasingly frustrating.
In the past I’d tried doing things like using an ssh tunnel to forward the
emacsclient connections from the remote host to my local machine, but
emacsclient doesn’t anticipate this case. The emacsclient protocol is very
simple: ASCII commands delimited with line feeds. So, for example:
$ emacsclient /foo/bar
# and this sends (approximately):
-dir /foo -file /foo/bar
The problem is that this is being sent to your local emacs, which then tries
to load the local/foo/bar. A TRAMP path is in the form of (approximately)
/ssh:example.com:/foo/bar, so if only this could be prepended, emacs could
deal with it!
It wasn’t automatic. It’s really convenient to be able to have TRAMP dump
the appropriate authentication file onto the server as soon as a connection
is made without having to connect using a special function.
It was relying on emacsclient sending a -tty argument in a specific
format, and then using the TRAMP prefix of the current buffer. In practice
this means you have to have your frontmost buffer a file open on the remote
server, and only then can you use emacsclient on that remote server.
I started to hack on it, but I was still affected by the same original problem:
emacsclient doesn’t know about TRAMP. The default implementation doesn’t even
have a way of specifying additional data to be sent over the wire. With the
simplicity of the emacsclient protocol, it started looking like writing my own
would be the simplest choice.
So, I put together some elisp to write out a special client authentication file
that would also include the TRAMP prefix and a shell script that would parse it
and write commands using nc. My elisp implementation borrows code from Ryan
Barrett’s (and thank you for getting me started with this!) but goes a bit
farther. The full workflow is like this:
Update your .emacs to load and configure the elisp.
Update your .profile to set $EDITOR to the emacsclient.sh script and
tell it where the authentication file is.
Open a TRAMP connection from your local emacs to write out the
Go hog wild.
Repeat only steps 3-5.
Here are the versions of emacsclient.sh and remote-emacsclient.el as of
(require'tramp)(require'tramp-sh)(setqserver-use-tcpt)(server-start)(defunput-alist(keyvaluealist)"Set cdr of an element (KEY . ...) in ALIST to VALUE and return ALIST.If there is no such element, create a new pair (KEY . VALUE) andreturn a new alist whose car is the new pair and cdr is ALIST."(let((elm(assockeyalist)))(ifelm(progn(setcdrelmvalue)alist)(cons(conskeyvalue)alist))))(defunupdate-tramp-emacs-server-port-forward(method-name)"Update the specified TRAMP's method to forward the Emacs server port to the local host. This lets emacsclient on the remote host open files in the local Emacs server."(let*((method(assocmethod-nametramp-methods))(ssh-args(cadr(assoc'tramp-login-argsmethod))))(put-alist'tramp-login-args(list(put-alist"-R"(let((port(process-contactserver-process:service)));; put-alist makes a dotted pair for the key/value, but tramp-methods;; needs a normal list, so put the value inside a list so that the;; second part of the dotted pair (ie the cdr) is a list, which;; converts it from a dotted pair into a normal list.(list(format"%s:127.0.0.1:%s"portport)))ssh-args))method)))(defuntramp-make-tramp-file-name-from-vec(vecfile)"Convenience function for making a TRAMP path, since thisapparently didn't already exist."(tramp-make-tramp-file-name(tramp-file-name-methodvec)(tramp-file-name-uservec)(tramp-file-name-hostvec)file))(defcustomtramp-default-remote-emacsclient-auth-file"~/.emacs.d/remote-server""Default remote path at which to save the remote emacsclientauthentication file. This can be a string or nil to disablesaving an authentication file.The authentication file is similar to the one written out by theemacsclient TCP server, except it includes the prefix used forthe TRAMP connection to the remote server.":group'tramp:type'(choice(constnil)string))(defcustomtramp-remote-emacsclient-auth-file-alistnil"The remote emacsclient authentication file path to use forspecific host/user pairs. This is an alist of items (HOST USERPATH). The first matching item specifies the path to use for aconnection which does not specify a method. HOST and USER areregular expressions or nil, which is interpreted as a regularexpression which always matches. If no entry matches, thevariable `tramp-default-remote-emacsclient-auth-file' takeseffect.If the connection does not specify the user, lookup is done usingthe empty string for the user name.See `tramp-default-remote-emacsclient-auth-file' for anexplanation of the auth file path.":group'tramp:type'(repeat(list(choice:tag"Host regexp"regexp(constnil))(choice:tag"User regexp"regexp(constnil))(choice:tag"emacsclient auth path"string(constnil)))))(defuntramp-get-remote-emacsclient-auth-file(vec)"Determine the full TRAMP path for the remote emacsclientauthentication file, given a connection vector."(let((auth-file(let((choicestramp-remote-emacsclient-auth-file-alist)(host(or(tramp-file-name-hostvec)""))(user(or(tramp-file-name-uservec)""))lfileitemmatched)(whilechoices(setqitem(popchoices))(when(and(string-match(or(nth0item)"")host)(string-match(or(nth1item)"")user))(setqlfile(nth2item)choicesnilmatchedt)))(ifmatchedlfiletramp-default-remote-emacsclient-auth-file))))(ifauth-file(tramp-make-tramp-file-name-from-vecvecauth-file))))(defuntramp-save-remote-emacsclient-auth-file(&optionalvec)"Write the remote emacsclient authentication file for a givenconnection buffer, or, if used interactively, for the TRAMPconnection of the current buffer."(interactive)(let((vec(orvec(tramp-dissect-file-namedefault-directory))))(condition-caseerr(let((auth-file(tramp-get-remote-emacsclient-auth-filevec))(server(process-contactserver-process:local)))(ifauth-file(with-temp-fileauth-file(insert(format"127.0.0.1 %d\n"(eltserver(-(lengthserver)1)))(format"-auth %s\n"(process-getserver-process:auth-key))(server-quote-arg(tramp-make-tramp-file-name-from-vecvec""))"\n"))(when(called-interactively-p'any)(message"No remote emacsclient auth file for %s"default-directory))))(file-error(message"error saving remote emacsclient auth: %s"err)))))(defadvicetramp-open-connection-setup-interactive-shell(aftercopy-server-file-by-tramp(procvec)activate)"Automatically write out a remote emacsclient auth file after asuccessful connection."(tramp-save-remote-emacsclient-auth-filevec))(provide'remote-emacsclient)
The update-tramp-emacs-server-port-forward function takes a TRAMP method
and updates the ssh arguments to that method to include a -R flag forwarding
remote TCP connections to the local server. It’s probably not necessary to call
it for any TRAMP method other than your default.
By default every new server connection will leave an authentication file.
This can be tuned with the tramp-default-remote-emacsclient-auth-file and
tramp-remote-emacsclient-auth-file-alist variables. The former can be set to
nil to disable auth file creation by default. The latter is in the same form
as the tramp-default-method-alist variable for specifying the location of the
auth file by host and/or by user.
$remote_emacs_auth’s default mirrors remote-emacsclient.el’s default of
writing the auth file to ~/.emacs.d/remote-server. Since emacsclient.sh is
(for me, by default) checked out in ~/.dotfiles and set executable, this is
all that’s required to use it as an editor. As an added bonus, the emacs
alias lets me type emacs somefile over ssh to open somefile in my local
There’s only two caveats (that I’m aware of (I’ve only tried the scp, ssh,
and scpc tramp methods with this so far)):
Since the authentication key changes every time emacs is started, you must
open a new tramp connection to a server before emacsclient will be able to
speak to your local emacs. I haven’t found a good solution to this other
than forcing the authentication key to always be the same (bad) or making
emacs reconnect to everything whenever you start it (worse).
There’s no detection of when the server is restarted. Restarting the server
in a running emacs also changes the authentication key. Making a new tramp
connection isn’t required in this case; there’s a
tramp-save-remote-emacsclient-auth-file function that can be run
interactively. Running it will write a new auth file for the connection
corresponding to the current buffer.