The Contract: Streams In, Choices Out
For fzf, the most consequential design decision often happens before the prompt appears: deciding what counts as one candidate record and which program is responsible for producing it. This chapter focuses on fzf's input and output contract—how it chooses a source, preserves record boundaries, exposes fields for searching and display, and returns a selection to the next command. Understanding that contract lets you build workflows in which the selected value is both meaningful to a person and usable by the consumer that follows.
Where the candidates come from
The simplest fzf invocation receives a stream through standard input. A producer prints candidates, fzf reads them, and the selected record is written to standard output. That is the ordinary pipeline, but it is not the only way input can arrive. fzf changes its source behavior according to the environment around it.
When a command or pipe supplies input, that stream is the authority. fzf does not silently replace it with a directory listing. If there is no input pipe and standard input is a terminal, fzf can use FZF_DEFAULT_COMMAND as a command that generates candidates. If that variable is not set, the built-in directory walker becomes the producer. The walker can be configured to include files, directories, hidden entries, or symbolic links, and it can start from chosen roots while skipping selected directories. In other words, standalone fzf has a convenient built-in source, but the source remains a distinct stage in the workflow.
That distinction matters when the same binary is invoked in different contexts. A person who types fzf at a prompt may see paths from the walker. A person who runs ps -ef | fzf has supplied process records instead. A shell widget may set a task-specific command and task-specific options before invoking fzf. The interface looks similar in all three cases, but the candidates have different meanings and require different downstream handling.
The source decision follows the input channel as well as the configuration. A non-terminal standard input is treated as the stream to read; an interactive terminal leaves fzf free to use a configured default command or, when none exists, the walker. That prevents an invocation already attached to a producer from unexpectedly starting a second producer. It also means that moving the same command into a shell widget can change its source even when the visible fzf options remain unchanged. The shell adapter may intentionally clear or replace the standalone defaults because it already knows whether it is gathering paths, directories, or history.
There is a simple failure boundary here: fzf cannot create a meaningful candidate set when the producer emits nothing, cannot recover a record that the producer split incorrectly, and cannot infer repository or application policy from an unlabelled path. An empty result may be a correct consequence of the source, not evidence that fuzzy matching failed. Diagnosing the workflow therefore starts upstream—inspect what the producer emits and when it emits it—before changing the prompt or ranking settings.
The reader in fzf treats the stream as records separated by a delimiter. Newline is the default, which is convenient for ordinary one-line entries. It is not a safe universal assumption. A file name can contain a newline, and an application may intentionally use multiline entries as one candidate. For those cases, --read0 makes the input delimiter the ASCII NUL character, while --print0 makes selected output NUL-delimited. The input and output choices are related but independent: a workflow must arrange for both the producer and consumer to agree on the boundary they use.
The important idea is that fzf does not need to understand a record's domain to preserve it. It can carry a path, a process row, a log event, or a formatted search result through the interaction. The producer decides how the record is represented; fzf decides which record a person selects; the consumer decides what the selected bytes mean. If the record boundary is wrong, later ranking and preview features cannot reconstruct the original value reliably.
The built-in walker illustrates both the convenience and the limit of a default producer. Its implementation uses a directory-walking library, supports configured roots and ignore patterns, and takes care not to follow certain symbolic links back into an ancestor of a root. That guard prevents a link such as one pointing from a small working tree toward a much larger ancestor from causing an unnecessarily broad traversal. But the walker is still a traversal mechanism, not a complete project-search policy. The project documentation recommends tools such as fd or ripgrep when a workflow needs their behavior, including traversal that respects .gitignore.
This is a useful division of labor. A traversal tool can decide what files belong in a candidate set. A content-search tool can decide which files contain a pattern. fzf can then provide the human-facing narrowing and selection step. Treating all three as the same operation makes a workflow harder to reason about and can put expensive work in the wrong place.
Shell integration makes the source boundary explicit rather than hiding it. The Bash and Zsh bindings use separate configuration for file selection, directory selection, and command history. CTRL-T is associated with files and directories, ALT-C with directories, and CTRL-R with history. The Fish and Nushell integrations make their own choices about native completion, history representation, NUL-safe output, and the shell's line-editing model. A global default command may apply to standalone fzf, yet the shell integration may deliberately use a task-specific command instead. That is not an inconsistency; it prevents a file-oriented default from being applied to a history or completion workflow by accident.
One record, three views
Once candidates enter fzf, a second boundary appears inside the record itself. A useful row often contains more information than a person should search or a consumer should receive. Consider a search result formatted as a path, a line number, and matching text, separated by colons. The path and line number are essential when opening the result in an editor. The matching text is usually the part a person wants to search. A selector that hides either distinction forces the producer or consumer to compensate with brittle string manipulation.
fzf provides three related controls for keeping those roles apart. --nth limits the fields used for matching. It answers, “Which parts of this record should the query inspect?” --with-nth changes how the row is presented in the interface. It answers, “What should the person see while making the decision?” --accept-nth determines what is printed after acceptance. It answers, “Which fields should cross the output boundary?” They may all refer to the same delimiter-separated row, but they solve different problems.
A git grep-style result is a useful example. The producer can emit path:line:number:text. Matching the entire row may cause path names and line numbers to dominate a query intended for source text. Limiting the search scope to the text fields makes the query more faithful to the person's intent. The visible row can still retain the path and line number, because those fields help identify the result. After selection, accepted output can be reduced to the path, or rearranged into the form required by the next command. The selector has shown a rich record without forcing the consumer to parse the display line blindly.
The controls are not merely presentation conveniences. With --with-nth, fzf retains the original input lines so they can be transformed again when the presentation changes. That retention costs memory, which is why the manual advises using the option when field transformation is actually needed. With a non-default delimiter, field expressions also become a contract: the producer's separators, the selector's interpretation, and the consumer's expectations must agree. A row that looks right on screen can still produce the wrong value if the accepted fields are not specified deliberately.
Imagine a row whose first field is a stable identifier, whose middle fields are descriptive text, and whose final field is a value intended for a command. The person may need to search the description, see the identifier beside it, and return only the command value. A single undifferentiated string cannot express those intentions cleanly. Field-aware controls let the display carry context while the output remains narrow. They also make changes easier to reason about: changing the visible arrangement need not change the identity that is sent downstream, and enabling multi-selection should be treated as changing the consumer's input from one record to a collection of records.
The delimiter is part of this agreement all the way to the consumer. A producer that uses NUL to keep multiline records intact must be paired with fzf's NUL-reading mode, and a consumer that expects NUL-separated output must be paired with --print0. Otherwise, a workflow may appear to work on ordinary examples while breaking on a legitimate name or multiline entry. The safe test is not whether the interface renders a row correctly; it is whether the complete producer–selector–consumer path preserves the record that the person actually selected.
The same principle applies to previews. A preview can show the contents of the current file, a highlighted line, a process detail, or another form of context, but the command behind it must match the candidate type. The project documentation warns against putting a file-specific preview such as a syntax-highlighting command into global defaults. Standalone fzf can receive process listings, numeric sequences, or history, for which that preview would be meaningless or fail. A preview belongs close to the workflow whose producer guarantees the assumptions it needs.
This is also why the phrase “fzf input” should be treated as more than a list of strings. It is a temporary interface between programs. The record needs a stable delimiter, useful fields, and an output form that the consumer can interpret. The human-facing view may be richer than the machine-facing value, but the transformation must remain explicit. If the consumer needs a path and a line number, the workflow should retain those fields and pass them intentionally; if it needs only a command or identifier, the accepted output should not include decorative context by default.
There is a related boundary around the file walker. fzf can traverse files and directories for a quick standalone selector, but an external producer remains responsible for specialized inclusion rules, repository semantics, content indexing, or other domain decisions. The evidence in the project supports the walker's configured traversal and symlink safeguards; it does not establish that it implements every policy provided by external search tools. Choosing fd, ripgrep, or another producer is therefore a way to choose a different source contract, not a failure of the selector.
The output side has the same shape. On normal acceptance, fzf writes the selected record or records. Multi-selection changes the possible output from one value to a set, and NUL-delimited output changes how the consumer must read it. A consumer can be a shell command, an editor, a script, or another pipeline stage, but it should not have to guess whether the displayed row contains metadata that is safe to pass along. The safest handoff is the one whose boundary is visible in the workflow itself.
That visibility gives a practical test for every new fzf use case. First identify the producer and ask what one candidate means. Then choose the delimiter and decide which fields deserve search attention. Keep enough information visible for a sound human choice, but use an explicit accepted form for the next command. Finally, check that defaults such as the walker, completion source, or preview do not smuggle in assumptions from a different kind of list.
Once those choices are settled, the next problem is ranking. The same records can produce very different decisions depending on whether a query is fuzzy, exact, anchored, inverse, or field-scoped, and depending on whether path names or chronological order should matter. fzf's contract brings the right records to the prompt; the next question is how the prompt decides which of those records deserves to rise to the top.