Using fzf inside an SSH session to search files, browse history, and select processes on a server, plus batch operation patterns with fzf and xargs. Including hybrid workflows that combine local lists with remote actions, and managing many hosts via completion.

In episode 12 you learned to place fzf in a tmux and Zellij popup — letting the search interface float above your work instead of overwriting the screen. Now let's drag in the next question: what if that work is not on your machine, but on a server? An infrastructure engineer rarely works on a single machine; they hop between app servers, database hosts, and jump boxes almost all day.
This episode covers using fzf in the remote world: running fzf directly inside an SSH session to search files, browse history, and select server processes; using the fzf + xargs pattern for safe batch operations; combining local search results with remote actions in a hybrid workflow; and managing dozens of hosts with fzf-based completion.
The first principle to hold on to: fzf doesn't care where the item list comes from. As long as it receives text lines on stdin and prints a selection on stdout, the source can be a local machine, a remote machine, or even a cache file. That's the key to this whole episode.
Many assume fzf is a local tool, but it's just an ordinary terminal process. As long as SSH provides a TTY (which it always does for interactive sessions), fzf runs fine on the other end. The difference is not in fzf itself, but in where the preview command executes: if fzf runs on the server, its preview reads the server's files and processes; if fzf runs locally, the preview must know how to reach the server.
Here's the mental model: run fzf as close as possible to the data you want to see. Want to select a process on the server? Run fzf on the server so its preview can call ps directly. Want to pick log lines you've already pulled locally? Run fzf locally so the preview is fast, with no network round-trip.
The simplest case: search files on a server with fd or find, then select interactively. Because fzf runs on the server, everything is local to the server — including the preview:
ssh deploy@app01 'fd . /srv -t f | fzf --preview "head -n 50 {}"'Notice the nested quoting: single quotes for the whole remote command so your local shell doesn't process it, then double quotes for the preview command. If fzf isn't installed on the server yet, a single apt install fzf or running its official installer works — just like on a local machine.
Tip
Searching with find in a large directory can feel slow because the list is built in full before fzf opens. On servers that already have fd, results are much faster because fd does parallel traversal. On servers without fd, the common practice is limiting depth (-maxdepth 3) or using find together with file type filters.
Two of the most common operational tasks on a server — browsing command history and selecting processes — are also perfect fzf use cases. Sorted history removes duplicates so you don't wade through the same lines repeatedly:
ssh deploy@app01 'cat ~/.bash_history | sort -u | fzf --height 40% --layout reverse'Selecting processes is far more powerful when fzf runs on the server, because its preview uses ps on the same machine. Here the {1} placeholder takes the first field — the PID column:
ssh deploy@app01 'ps -eo pid,user,comm | fzf --header "Pilih proses" --preview "ps -fp {1}"'This is analogous to an interactive top, but with fuzzy power: type nginx and jump straight to the nginx process, without pressing a search key even once.
Now the most valuable pattern: select many items, then execute one command on all of them. The fzf --multi + xargs combination is the backbone of batch operations. The following example selects several Docker containers on a server then restarts them all:
ssh deploy@app01 'docker ps --format "{{.Names}}"' \
| fzf --multi --height 40% \
| xargs -I{} ssh deploy@app01 'docker restart {}'xargs -I{} replaces {} in the command with one selected item — so the remote command executed looks like ssh deploy@app01 'docker restart web-api'. The same pattern applies to restarting systemd services, deleting old log files, or fetching several files with scp.
Warning
xargs splits arguments by spaces and newlines by default. If your item list is file names that may contain spaces, use the null delimiter: fzf --multi --print0 | xargs -0 -I{} ssh host "command {}". This principle is exactly what we discussed about --read0 and --print0 in episode 6 — here the stakes are higher because a parsing error ends up as a command executed on a server.
Not every step has to run in one place. A hybrid pattern that's often used: the item list is built and selected locally, the action executes remotely. This saves latency because fzf doesn't wait on the network every time the query changes, and it gives you a responsive local preview.
For example, pull a log list into a local file once, then select with a preview that reaches the server only for the highlighted lines:
ssh deploy@app01 'find /var/log -name "*.log" -mtime -7' > /tmp/app01-logs.txt
cat /tmp/app01-logs.txt | fzf --preview 'ssh deploy@app01 "tail -n 30 {}"'Notice the trade-off: building the list once via find on the server is cheap, but a preview that calls ssh on every cursor move can feel slow. For large lists, limit the preview command with ssh -o ConnectTimeout=2 or switch to the "run fzf on the server" pattern from the first example.
Important
Rule of thumb for choosing the execution location: run fzf on the side where the data is accessed most often. Data changes fast and is large on the server? fzf on the server. Data is a snapshot already pulled locally? fzf locally. Placing fzf on the wrong side is the most common cause of "why does remote fzf feel slow".
The more servers you have, the longer you spend typing host names. The host list from ~/.ssh/config is a perfect fzf candidate: built from a single file, structured, and used every day. The simple function below replaces memorizing host names with a fuzzy dialog:
sshp() {
local host
host=$(grep '^Host ' ~/.ssh/config | awk '{print $2}' \
| grep -v '\*' | fzf --prompt 'SSH host> ' --tmux center,50%)
[ -n "$host" ] && ssh "$host"
}Type sshp, then filter host names — type prod to leave only the production hosts. Combining with --tmux from episode 12 makes this dialog float above whatever you're working on, and Enter jumps straight to the target server.
| Mistake | Symptom | Solution |
|---|---|---|
| Running fzf locally but the preview needs server files | Empty preview or error | Run fzf on the server, or call ssh inside the preview |
xargs without -0 on a space-containing list | Arguments split, wrong command | fzf --multi --print0 | xargs -0 -I{} ssh host "cmd {}" |
| Rebuilding the remote list every time the preview changes | Feels slow due to network round-trips | Cache the list to a local file, limit on-demand preview |
| Typing host names manually | Typos, SSHing to the wrong host | Use fzf completion from ~/.ssh/config |
Forgetting that {} in the preview is executed by the remote shell | Command doesn't behave as expected | Test the preview on a single item before wide use |
In this episode 13 you settled one important question: fzf knows no machine boundaries. You can run it directly on a server over SSH to search files, browse history, and select processes with a preview that reads the server directly; use the fzf --multi + xargs -I{} pattern for batch operations; combine local lists with remote actions in a hybrid workflow; and manage many hosts with fzf completion from ~/.ssh/config.
The principle to take home: place fzf on the side where data is accessed most often, and treat items as data the shell can never be trusted with.
That last sentence is not small talk — it actually opens the door to the next episode. The more fzf executes commands for you (preview, execute, become), the bigger the surface where something with bad intentions can get in. In episode 14 we cover security & best practices: the command injection risks behind --preview and --bind, how to write safe fzf commands, and protecting privacy — so the tool that speeds up your work doesn't become a doorway to trouble.