Bringing item lists to life with the reload action, the change event that follows the query, periodic refresh scheduling, and cross-reload tracking with --track and --id-nth. Closing with controlling fzf from external processes via the --listen HTTP server and Unix domain sockets for automation and tooling.

In episode 15 you chained actions with --bind — including reload, which you used to update a git add list without leaving fzf. That episode gave living lists a touch. Episode 16 opens all the taps: how to make fzf lists truly alive — changing to follow the query, refreshing periodically, keeping track of the same items across reloads, and even being controlled by another program entirely outside fzf.
Three areas to conquer. First, dynamic reload: the reload action, the change event that triggers a reload when the query changes, and periodic refresh scheduling for lists that keep moving, like a process list. Second, cross-reload tracking: the --track and --id-nth options since fzf 0.71 so the cursor doesn't "jump" to another item every time the list updates. Third, --listen: fzf's built-in HTTP server that lets external processes send actions over HTTP and Unix domain sockets — the gateway to automation and tooling.
reload: Replacing the List on the FlyThe reload(command) action replaces the entire item list with the output of the given command — without closing fzf. The cursor stays in place, the query stays as it is, only the list changes. This is like swapping the tray of goods on a conveyor: the work is the same, the tray's contents are new.
ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)'The reload-sync variant does the same but waits for the command to finish before continuing — important when the next step depends on the new list's results, for example with --multi where the selection must be remapped onto the new items.
Note
The difference between reload and reload-sync is not about speed, but synchronization. reload runs asynchronously: fzf forwards the input and keeps working. reload-sync blocks until the process finishes. For large lists or expensive commands, reload-sync makes the UI "wait" — choose according to need, not randomly.
change: Reload That Follows the QueryThe change event fires every time the query changes. Combining it with reload produces the most famous pattern in the fzf ecosystem: delegating search to ripgrep. fzf no longer filters on its own; it sends the query to rg and displays the results. That's why fzf is relieved of the filtering job with --disabled:
fzf --disabled --bind 'change:reload:rg -l {q} || true' \
--preview 'bat --color=always {}'Reading it: every time you type, fzf runs rg -l <query>; the matching lines enter as a new list; || true prevents an error when nothing matches; and the preview opens the selected file with bat. The result: a code search that was previously limited by list size can now dive through an entire repository. This is an important distinction: fzf stays fast not only because of its algorithm, but because you can move the load to a more specialized tool.
Not every list waits for interaction. Process lists, logs, or pod statuses deserve to update themselves periodically. That's where the every(N) event comes in: an event fired every N seconds, available in fzf releases 0.73 and up. A process list that refreshes itself every two seconds looks like a filterable htop:
fzf --header-lines 1 --track --id-nth 2 \
--bind 'start,every(2):reload-sync:ps -ef'Tip
Note the two options in the example above: --track --id-nth 2. Without them, every reload resets the cursor to the first item — uncomfortable when you're aiming at one process in the middle of the list. With --id-nth 2, fzf identifies items by the second field (the PID column in ps -ef), then tracks the same line in the new list. This is the feature we cover next.
--track and --id-nthBefore fzf 0.71, a reload was always treated as "a brand-new list with no connection": multi-selection was lost and tracking was dead. --id-nth changes that by defining an identity field — a field that doesn't change between reloads even though other columns do.
Imagine monitoring a process list: the CPU and memory columns change every second, but the PID never changes. With --track --id-nth 1 on a list whose first field is the PID, fzf can find the same process in the new list and keep the cursor there:
fzf --track --id-nth 1
fzf --track --id-nth ..The semantics of --id-nth are the same as --nth from episode 6: a number points to the N-th field with the default whitespace separator, and .. means the whole line. With --multi, selected items are also preserved across reload-sync as long as their identity matches — the selection no longer vanishes the moment the list updates.
--listen: Controlling fzf from OutsideNow the most interesting part for automation. The --listen[=ADDR:PORT] option runs an HTTP server inside fzf — since fzf 0.52 — accepting two kinds of requests: POST to send actions (exactly the ones you bind in --bind), and GET to read the program's state as JSON.
fzf --listen 6266From another terminal — or from a script, cron, or webhook — you can send actions:
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
curl localhost:6266 | jq .Sent actions can chain with + exactly like in --bind: reload(seq 100) replaces the list, change-prompt(hundred> ) changes the prompt. Imagine a real scenario: a script waits for a build to finish, then triggers reload on an fzf that's showing a build-artifact list — the user doesn't need to press anything.
$FZF_PORTSpecifying a port manually risks collisions. Without a number, fzf picks a free port itself and exposes it as the $FZF_PORT variable to its child processes:
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'curl "localhost:$(cat /tmp/fzf-port)" -d 'preview:echo siap'This is a reliable pattern for tooling: fzf picks its own port, tells the world through a file, and other scripts connect without guessing.
For communication on the same machine, Unix domain sockets are tidier than TCP — no network address, no port that can be scanned, just a path. If the --listen argument ends in .sock, fzf creates a socket at that path (behavior available since 0.66 and now a stable path in the 0.74 series we use), then exposes the path as $FZF_SOCK:
fzf --listen /tmp/fzf.sockcurl --unix-socket /tmp/fzf.sock http -d up--listenAn open HTTP server is a door — and a door needs a lock. Two mechanisms protect --listen:
$FZF_API_KEY — if this variable is set, all requests must carry an x-api-key header with the same value.--listen only accepts local connections. Accepting connections from the network requires a non-localhost address and an API key, or --listen-unsafe which disables the protections — a fitting name for something that's genuinely unsafe.export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
fzf --listen 6266curl localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'Warning
Exposing --listen to the network is a step that should be refused unless truly necessary. The actions that can be sent to fzf — including reload and execute — are command-execution capabilities. Binding them to an address accessible from other machines without strong authentication is like opening a back door. On shared machines, make a habit of --listen with an API key, or use a Unix socket that's invisible to the network.
--listen's full power shows when fzf becomes part of a larger system, not a standalone app. Three patterns that are immediately useful:
reload) when files change — fzf becomes a launcher panel controlled from outside.reload and change-prompt(sukses> ) — build status appears inside fzf without a manual refresh.--expect on the reading side is no longer needed because --listen allows sending equivalent actions from afar.| Mistake | Symptom | Solution |
|---|---|---|
change:reload without --disabled | Double results: fzf and rg both filter | Turn off fzf filtering with --disabled |
Forgetting || true in an rg reload | fzf closes when there are no results | Add || true at the end of the command |
| Cursor jumping on every reload | Selection lost across updates | Use --track --id-nth on an identity field |
| Port collision | fzf can't start the server | Let fzf pick a port and read $FZF_PORT |
--listen without an API key on a shared machine | Anyone can send actions | Set $FZF_API_KEY and send the x-api-key header |
Treating .sock as a port | Socket doesn't form | A path ending in .sock = Unix socket, not a port |
In this episode 16 you brought fzf to life from many directions: reload and reload-sync for replacing lists in place; the change event making the query the control for an external search through ripgrep; periodic refresh scheduling with every(N); cross-reload tracking with --track and --id-nth so the cursor and selection don't jump; and --listen opening fzf as an HTTP server and Unix socket with $FZF_PORT, $FZF_SOCK, and $FZF_API_KEY — fully controlled by external processes.
The message to take home: fzf is no longer a single player — it's a component that can be plugged into and controlled from a larger system.
All this power, however, still depends on one practical question: how fast can fzf process the lists you give it? When lists swell to hundreds of thousands or millions of items, everything comes back to performance. In episode 17 we cover performance & large datasets: choosing an algorithm with --algo, limiting items, the --sort decision, minimizing the preview load, and strategies for millions of items in modern fzf, whose scaling is linear across CPU cores.