Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Transferring files

Ctrl-t on a host opens a dual-pane transfer screen: your local files on one side, the host’s on the other. Mark what you want and send it in either direction over SFTP, with fuzzy search on both sides, live progress, and a F7 to create directories without leaving.

sshelf authenticates once: it opens an ssh ControlMaster that reuses the host’s normal auth (keys/agent/ProxyJump, or the stored password, supplied the same way as on connect) and runs sftp over it. No per-file re-prompts, and ~/.ssh/config is never touched. Remote listing and transfers run on a background thread, so the UI stays responsive on slow links.

Keys

KeyAction
typefilter the focused pane
Tabswitch the focused pane (local ↔ remote)
↑ / ↓, Ctrl-p / Ctrl-nmove the selection
Spacemark / unmark the selected file or folder
Ctrl-amark everything the filter shows; press again to clear every mark
Ctrl-ssend the marked entries (or, with none marked, the selected one) into the other pane’s directory
F7 / Ctrl-fcreate a directory in the focused pane
→ / Enteropen the selected directory (on a file: send it)
←go up a directory
Backspaceedit the filter, or go up when it’s empty
Esccancel a running transfer, else clear marks, else clear the filter, else close the screen

Marking and sending several at once

Space marks the entry under the cursor; marked rows get a • and the accent color, and the pane title counts them. Ctrl-s then sends all of them, files and folders alike and folders recursively, into the other pane’s current directory, one at a time through the same single authenticated connection. The progress line counts through the batch (2 of 5 report.pdf → deploy@host).

  • Marks are positional. Changing directory, refreshing a listing, or a listing error drops them; they are never remembered per path. Esc clears them explicitly.
  • Sending consumes the marks; the queue becomes the record of what’s going.
  • An entry the destination already has is skipped and the queue carries on; the summary names what was passed over (sent 3 of 4 · skipped dup.txt (already there)). A real transfer failure stops the rest, since whatever broke will usually break the next one too, and the status says how many were left unsent.
  • Space marks rather than typing a space into the filter. Filenames containing spaces still match by the rest of their name.

Creating a directory (F7)

F7 (or Ctrl-f, if your terminal keeps F7 for itself) opens a one-line input at the bottom of the focused pane. Type a name, Enter creates it in that pane’s current directory, Esc cancels. It works on both sides; the remote one goes through the same SFTP connection.

The name must be a single directory name: no / (this creates one directory, not a path), no control characters, not . or .., and not a name that already exists. An existing directory is never adopted, so the input stays open with an error and you can pick another name. On success the listing refreshes and the new directory lands under the cursor.

Hidden files

Both panes list hidden entries. The local pane always did; the remote one lists with ls -lan through sftp, so dotfiles and dot-directories show up on the server side too and you can open .config or .ssh the same way you open any other directory. . and .. are never listed on either side (← goes up).

There is no show/hide toggle. If a directory has too much in it, type a . into the pane filter: that keeps the names with a dot in them, and the hidden ones sort to the top.

Behavior & limits

  • Directories are shown as name/ and symlinks as name@, and symlinks are skipped.
  • A same-named file or folder already present in the destination is skipped (with a message), never overwritten. What that promise rests on differs between a file and a folder, so it is spelled out under What “never overwritten” covers below.
  • One transfer runs at a time: a batch is a queue, not parallel copies. Single-file downloads show bytes + percent; folders and uploads show as in-flight (cancelable with Esc, which abandons the rest of the queue too).
  • Filenames are shell-quoted (spaces are fine) and control characters are stripped from display.
  • A remote listing is given 60 seconds and a remote mkdir 30 seconds before sshelf kills the sftp running it, and the pane says timed out after 60s listing <path> rather than sitting there. A listing is also capped at 16 MiB of output and 50,000 entries; past that the pane says listing truncated at 50000 entries instead of passing a partial directory off as the whole of it. Closing the screen never waits on any of this: Esc gets the terminal back even if the server has stopped answering.
  • The connection uses StrictHostKeyChecking=accept-new, like connect: a first-time host key is trusted on first use, a changed key still hard-fails. See Security.
  • Renaming, deleting, changing permissions, and overwriting are not in this version.

What “never overwritten” covers

A single file never replaces anything, sent in either direction. The bytes land on a private .sshelf-part-… name in the destination directory first, and the finished file is put in place with a link, which fails if any name is already there. A symlink counts as a name, and it is never followed, so nothing can redirect the write. If the name turned up while the transfer was running, the entry is skipped exactly as the pre-flight check would have skipped it, and the queue carries on.

On an upload that link is made by the server, through the hardlink@openssh.com extension OpenSSH has carried since 5.7. Two kinds of destination cannot take one. A server that does not offer the extension is the first; a filesystem with no hard links at all is the second, which locally means exFAT and FAT32 (what a USB stick usually is) and a fair number of SMB and FUSE mounts. In both cases sshelf checks that the name is still free and then moves the temporary onto it, which is a smaller window than writing the final name directly but not the same guarantee. The bytes are never thrown away over it.

A folder is checked against the last listing of the destination and nothing more, whichever way it is going. A directory cannot be installed from a temporary with a link, so there is nothing better to be had. That listing is the one the pane is showing: sshelf asks the server for a fresh one immediately before each upload, so a folder queued behind another item is checked against a listing taken moments earlier, while the first item of a send is checked against whatever the pane last showed, which can be minutes old. A file that appears in the destination after that listing can be overwritten. If that matters for what you are sending, look at the destination first.

A remote directory past the 50,000-entry cap is the one case where the check cannot be made at all, so sshelf refuses to send a folder into it rather than guess:

the destination listing is incomplete (cut at 50000 entries) — sshelf can't promise not to overwrite there

Single files are fine there, in both directions, since they do not rely on the listing.

Where the connection lives

The screen holds one ssh ControlMaster, and its control socket lives in a directory sshelf creates for that session with mode 0700: $XDG_RUNTIME_DIR/sshelf/mux-<ulid>/m.sock when XDG_RUNTIME_DIR is set, otherwise ~/.local/share/sshelf/run/mux-<ulid>/m.sock. It used to be a predictable name straight in /tmp, where another account on the machine could take the path first. Both the socket and the directory are removed when the screen closes. A stray mux-* directory from a crash is harmless and can be deleted.

When it can’t connect

The screen opens one connection and cannot prompt for anything: sshelf is still holding the terminal, so a prompt from ssh would land on top of the TUI with no way to type into it. So the connection is opened with BatchMode=yes whenever there is no stored secret to supply, which means it fails quickly and says why instead of hanging (see D-032).

In practice a key host works here if the key has no passphrase, or if the key is already loaded in your agent (ssh-add -l to check). If it isn’t, you get “could not authenticate: if that key needs a passphrase, load it with ssh-add or save it on the host with ^e”. Do either and reopen the screen. A password host needs its password stored (^e), and a host that needs a verification code can’t open a transfer screen yet.

Debugging a failing transfer

The status line shows the underlying sftp error. For the full story:

sshelf --transfer-log ~/.local/share/sshelf/transfer.log     # or $SSHELF_TRANSFER_LOG

This appends every ssh and sftp command, the local and remote paths they touch, their stderr, and every value the host’s extra_args contributes. No password is logged: a stored secret reaches ssh via SSH_ASKPASS and never the command line. Taken together, though, that is a full description of the connection, so keep the file somewhere private. sshelf creates it mode 0600 and refuses to follow a symlink at that path (you get one line on stderr and no log), which is why the example points inside the data directory rather than at /tmp.