Understanding the fzf `--become` Action: How It Differs from Shell Piping

The --become action in fzf replaces the current process with a new command using syscall.Exec, preserving terminal state and avoiding the overhead and quoting issues of traditional shell piping.

The junegunn/fzf repository provides a powerful interactive fuzzy finder for the command line. Among its advanced features, the --become action stands out as a specialized key-binding mechanism that fundamentally changes how fzf hands off control to other programs.

What Is the fzf --become Action?

--become is a key-binding action that instructs fzf to replace itself with another program rather than merely launching a child process and returning to the selector. When triggered, fzf writes the target command to a temporary file, exits with a special status code, and allows a proxy process to execute the replacement.

In src/terminal.go, the action is defined as actBecome and routed through a reqBecome request when a binding like enter:become(vim {}) is triggered. The implementation posts the request to the main loop, which prepares the handoff.

The exit mechanism relies on the constant ExitBecome = 126 defined in src/constants.go. When fzf exits with this code, the proxy process (handled in src/proxy.go) recognizes the signal and reads the temporary *.become* file containing the command and optional environment variables.

Finally, Executor.Become in src/util/util_unix.go (and the Windows equivalent) calls syscall.Exec, which overwrites the current process image with the new command. The terminal file descriptors remain intact, and the new program inherits the same TTY.

How --become Differs from Shell Piping

Understanding the distinction between --become and traditional shell piping is crucial for choosing the right integration pattern. The differences span process models, terminal handling, and data passing.

Process Model

  • --become: fzf exits with code 126, the proxy reads the command file, and syscall.Exec replaces the process. No child process remains; fzf is completely gone.
  • Shell piping: The shell forks fzf as a child process, creates a pipe, and forks another child for the receiving command. Both processes run concurrently, and fzf returns its own exit status independently.

Terminal State

  • --become: The terminal is kept intact because the process image is replaced, not terminated. This avoids the "blank line" or cursor positioning issues that occur when fzf exits normally with Ctrl-C or empty selection.
  • Shell piping: The piped command must explicitly open /dev/tty if it needs terminal interaction, as its stdin is connected to the pipe, not the terminal.

Multi-Selection Handling

  • --become: Selections are safely expanded using the QuoteEntry helper in util_unix.go. Each selected item becomes a separate argument in the exec call, preserving spaces and special characters without additional quoting.
  • Shell piping: Lines are concatenated with newlines. Handling filenames with spaces requires xargs -0 or similar parsing, adding complexity and potential for errors.

Performance

  • --become: No extra fork/exec cycle, no pipe buffer overhead, and no I/O through pipes.
  • Shell piping: Requires an additional process spawn and data transfer through the pipe buffer.

Practical Examples of Using --become

Opening a Single File in Vim

The most common use case is opening a selected file directly in an editor without returning to the shell:

fzf --bind 'enter:become(vim {})'

When you press Enter, fzf writes vim <selected_path> to the temporary become file, exits with status 126, and the proxy executes syscall.Exec to replace the process with Vim. The terminal state is preserved exactly as it was under fzf.

Editing Multiple Files with Proper Quoting

For multi-selection workflows, use the {+} placeholder to pass all selected items as separate arguments:

fzf --multi --bind 'enter:become(vim {+})'

The QuoteEntry function in src/util/util_unix.go ensures that filenames containing spaces are properly quoted before being passed to the exec system call. This avoids the common pitfall of fzf | xargs vim where spaces break argument parsing.

Passing Custom Environment Variables

You can prepend environment variables to the command, which are parsed and passed to the new process:

fzf --bind 'enter:become(FOO=bar BAZ=qux vim {})'

In src/proxy.go, the proxy extracts these KEY=VALUE pairs into an environment slice (lines 44-46) and passes them to Executor.Become, which includes them in the syscall.Exec call. This allows you to configure the target program's environment without affecting the shell that launched fzf.

Summary

  • --become is a process replacement mechanism, not a subprocess spawn. It uses ExitBecome (126) and syscall.Exec to transform the fzf process into the target command.
  • Terminal state is preserved because the TTY file descriptors remain attached to the same process group, avoiding the blank-line issues common with normal exit.
  • Argument handling is safer than piping. The QuoteEntry helper ensures spaces and special characters in selections are handled correctly when expanded via {} or {+}.
  • Performance is optimal because there is no pipe buffer, no extra fork/exec cycle, and no shell interpretation overhead.
  • Source implementation spans src/terminal.go (action routing), src/constants.go (exit code), src/proxy.go (proxy logic), and src/util/util_unix.go (syscall execution).

Frequently Asked Questions

What exit code does fzf use for the become action?

Fzf exits with status code 126, defined as ExitBecome in src/constants.go. This specific code signals the proxy process that it should read the temporary *.become* file and execute the replacement command rather than simply returning to the shell.

Can I use become on Windows?

Yes, but the implementation differs. While Unix systems use syscall.Exec in src/util/util_unix.go to replace the process image, Windows does not support a direct equivalent of execve. The Windows implementation in src/util/util_windows.go simulates the behavior using process creation and exit handling, though the terminal state preservation characteristics may vary slightly from the Unix implementation.

How does become handle filenames with spaces?

The become action handles filenames with spaces safely through the QuoteEntry helper function in src/util/util_unix.go. When using placeholders like {} or {+}, fzf properly escapes each selection before passing it to syscall.Exec. This ensures that files with spaces, quotes, or other special characters are treated as distinct arguments by the target program, eliminating the quoting issues common with traditional xargs or shell piping approaches.

Is become better than execute for launching editors?

Generally yes, for launching interactive programs like editors. The execute action spawns a child process and waits for it to return, which can cause terminal state issues or require complex TTY handling. In contrast, become uses syscall.Exec to replace the fzf process entirely, preserving the terminal state and handing over control completely to the new program. This makes become ideal for workflows where you want to open a file in Vim, Emacs, or a debugger directly from the fzf interface without returning to the shell first.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →