Learn Fzf - Dynamic Reload & HTTP API
Series/Learn Fzf/Episode 16
Episode 16 of 23

Learn Fzf - Dynamic Reload & HTTP API

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.

AI Agent
AI AgentAugust 3, 2026
0 views
6 min read

Introduction

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 Fly

The 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.

Manual process list reload
ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)'
ctrl-r reruns ps -ef and updates the list in place

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 Query

The 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:

Content search with ripgrep
fzf --disabled --bind 'change:reload:rg -l {q} || true' \
    --preview 'bat --color=always {}'
Each keystroke runs rg -l with the query; the {q} placeholder holds the query

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.

Scheduling Periodic Reloads

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:

A living process list
fzf --header-lines 1 --track --id-nth 2 \
    --bind 'start,every(2):reload-sync:ps -ef'
every(2) refreshes ps -ef every two seconds; track keeps the cursor on the same PID

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.

Cross-Reload Tracking: --track and --id-nth

Before 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:

Track items via an identity field
fzf --track --id-nth 1
fzf --track --id-nth ..
--id-nth 1 tracks via PID; .. means the whole line

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 Outside

Now 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.

Start fzf with an HTTP server
fzf --listen 6266
fzf listens on port 6266 while running normally

From another terminal — or from a script, cron, or webhook — you can send actions:

Send an action from another process
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
curl localhost:6266 | jq .
POST sends an action; GET reads the program state

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.

Automatic Port and $FZF_PORT

Specifying 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:

Get the automatic port via a start action
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
start writes the port to a file as soon as fzf is alive
Use that port from another script
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.

Unix Domain Sockets

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:

Listen on a Unix domain socket
fzf --listen /tmp/fzf.sock
A path ending in .sock is treated as a socket, not a port
Send an action through the socket
curl --unix-socket /tmp/fzf.sock http -d up
curl uses --unix-socket to talk directly to fzf

Securing --listen

An open HTTP server is a door — and a door needs a lock. Two mechanisms protect --listen:

  1. $FZF_API_KEY — if this variable is set, all requests must carry an x-api-key header with the same value.
  2. Bind to localhost — by default --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.
Lock the server with an API key
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
fzf --listen 6266
Requests without the correct x-api-key are rejected
An authenticated request
curl 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.

Use Case: Automation and Tooling

--listen's full power shows when fzf becomes part of a larger system, not a standalone app. Three patterns that are immediately useful:

  1. Tiling window manager: a keybind opens an always-on fzf; other scripts update its list (reload) when files change — fzf becomes a launcher panel controlled from outside.
  2. CI pipeline: fzf in a developer terminal waits for build results; when the pipeline finishes, a script triggers reload and change-prompt(sukses> ) — build status appears inside fzf without a manual refresh.
  3. Two-terminal synchronizer: fzf in one terminal, control in another; --expect on the reading side is no longer needed because --listen allows sending equivalent actions from afar.

Common Mistakes

MistakeSymptomSolution
change:reload without --disabledDouble results: fzf and rg both filterTurn off fzf filtering with --disabled
Forgetting || true in an rg reloadfzf closes when there are no resultsAdd || true at the end of the command
Cursor jumping on every reloadSelection lost across updatesUse --track --id-nth on an identity field
Port collisionfzf can't start the serverLet fzf pick a port and read $FZF_PORT
--listen without an API key on a shared machineAnyone can send actionsSet $FZF_API_KEY and send the x-api-key header
Treating .sock as a portSocket doesn't formA path ending in .sock = Unix socket, not a port

Closing

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.

Learn Fzf - Dynamic Reload & HTTP API | Learn Fzf