Configuration
TL;DR: Put overrides in
~/.config/fut/config.toml, then pressCtrl-b Shift-R. Everything underuiis declarative.trusted_commands, extension directories, and trusted project recipes can execute programs and should be treated like shell scripts.
Fut’s global presentation configuration is safe, declarative, and
non-executable. Separately, trusted_commands, explicitly configured local
extension directories, and trusted project recipes are executable boundaries; see
Security boundary.
Fut refuses to start or attach another interactive Fut client from inside one of its terminals. If nesting is intentional, set FUT_ALLOW_NESTED in the command’s environment to force it, for example FUT_ALLOW_NESTED=1 fut.
Runtime location
Fut keeps its private Unix socket, lock, and daemon log under
$XDG_RUNTIME_DIR/fut when XDG_RUNTIME_DIR is an absolute path. Otherwise it
uses ${TMPDIR:-/tmp}/fut-UID. Set FUT_RUNTIME_DIR to an absolute directory
to override that location, FUT_SOCKET to an absolute socket pathname, or pass
--socket explicitly. Fut rejects empty or relative explicit overrides; empty
or relative XDG and temporary-directory values are ignored according to their
fallback semantics. Run fut doctor to inspect the resolved location without
creating it.
Location and lifecycle
Fut checks, in order:
config.tomlin the absolute directory passed with--config-dir;- the absolute path in
FUT_CONFIG; $XDG_CONFIG_HOME/fut/config.tomlwhenXDG_CONFIG_HOMEis absolute;~/.config/fut/config.toml.
A missing implicit file, including one selected through --config-dir, uses defaults and is not created. Pass --no-config to use built-in defaults without reading any configuration file; it cannot be combined with --config-dir. --config-dir and FUT_CONFIG must be absolute; FUT_CONFIG must exist. Configuration is loaded before an interactive client changes terminal state. Press Ctrl-b Shift-R to reload the invoking client’s global configuration and then the trusted project extension configuration for the session focused when reload began. Valid global bindings and layout commit first; if the project step fails, Fut keeps those global changes, leaves the project’s previous extension configuration active, and explicitly reports the partial success. Control commands and shell completion do not load UI configuration; fut doctor reads it without creating runtime state.
Files must be regular UTF-8 files no larger than 64 KiB. Unknown fields, invalid values, unsafe control or bidirectional-formatting characters, ambiguous segments, and out-of-scope tokens are errors.
Editor completion
Fut publishes a JSON Schema for global configuration.
Add this Taplo schema directive as the first line of config.toml to get
validation, documentation, and completion in compatible TOML editors:
#:schema https://fut.sh/schemas/config.json
The schema offers built-in action completion for [ui.bindings] and
[ui.hotkeys]. Installed extension command slugs and extension-owned settings
remain dynamic and are validated by Fut when it loads the configuration.
Closing a pane, tab, workspace, or session asks for confirmation by default. While the prompt is open, it exclusively owns keyboard, paste, and pointer input; resizing the host terminal remains available. Set ui.confirm_close = false to perform those close actions immediately. This setting is client-local and applies to both keyboard commands and contextual menus. Interactive rename and close actions can change only the session to which that client is attached and resources within it. A tab, workspace, or session close can finish only partway if a later terminal cannot close; Fut reports how many terminals exited and keeps the target marked closing because those exits cannot be undone.
Terminal-native alerts are configured at the top level. They are separate from coding-agent lifecycle notifications:
[alerts]
signal_outer_terminal = true # Optional; default false.
signal_outer_terminal is client-local and emits one BEL when a newly unseen
terminal bell arrives; whether that BEL is audible, visual, or ignored remains
under the containing terminal emulator’s control. It is deliberately off by
default.
Terminal scrollback
Fut retains up to 100 MiB of scrollback per terminal by default. Storage grows as output arrives; the full budget is not allocated up front. Set a smaller or larger budget in bytes:
[terminal]
scrollback_bytes = 104857600 # 100 MiB (default); 1073741824 is 1 GiB.
The budget measures terminal storage, not plain-text output or a fixed number of
lines. Actual storage is subject to internal page sizes and minimum screen storage.
Set 0 to disable scrollback.
Pane terminals use the configuration loaded when the daemon starts. Restart the
daemon to apply a changed budget to panes; client configuration reload does not
resize existing history buffers. Newly opened command popups use the client’s
current setting after Ctrl-b Shift-R. Alternate-screen applications manage
their own history.
Example
This example is intentionally customized; it is not a dump of the defaults. Omit any field to keep its default value.
#:schema https://fut.sh/schemas/config.json
extensions = [
"extensions/review-status",
"extensions/run",
]
[alerts]
signal_outer_terminal = false
[ui]
pane_layout = "splits" # "splits" or "accordion"
confirm_close = true # Require confirmation before closing panes, tabs, or workspaces.
prefix = "ctrl-b"
[ui.bindings]
open_command_bar = "space"
"run:restart" = "r"
# open_messages = "m"
# open_project = "S"
# reload_config = "R"
# reload_project_config = "P"
# enter_copy_mode = "["
# open_navigator = "s"
# open_agents = "a"
# open_left_sidebar = "w"
# open_right_sidebar = "]"
# open_tab_bar = "t"
# open_notifications = "u"
# focus_next_notification = "prefix"
# create_workspace = "C"
# create_tab = "c"
# rename_session = "S" # These rename and close actions have no default binding.
# rename_workspace = "W"
# rename_tab = "T"
# close_session = "q"
# close_workspace = "Q"
# close_tab = "X"
# focus_last_tab = "ctrl-t"
# focus_last_workspace = "ctrl-w"
# focus_last_session = "ctrl-s"
# close_pane = "x"
[ui.hotkeys]
# These run immediately, without the prefix.
# "ctrl-t" = "open_tab_bar"
# "ctrl-f" = "open_navigator"
# "ctrl-r" = "run:restart"
[trusted_commands.git_diff]
title = "Repository diff"
binding = "g"
program = "~/.dotfiles/tmux/tmux.symlink/git_diff_popup.sh"
# args = ["--optional-argument"]
size = { width = 120, height = 40 }
[ui.icons]
preset = "nerd_font" # "ascii", "unicode", or "nerd_font"
# current = "*" # Every icon may be overridden.
# closing = "x"
# overflow = "..."
# workspace = "W"
# tab = "T"
# zoom = "zoom"
# notification = "• " # Prefix before the unread notification count; spacing is preserved.
# vertical_divider = "|"
# pill_left = "" # Focused tab/workspace pill caps; empty outside "nerd_font".
# pill_right = ""
[ui.spinner]
style = "dots" # Browse the built-in styles with `fut --ui-playground`.
# interval = 80 # Optional frame delay in milliseconds.
# frames = ["-", "\\", "|", "/"] # Optional custom frames; overrides style.
[ui.styles.current]
foreground = "blue"
add_modifiers = ["reversed"]
remove_modifiers = ["underlined"]
[ui.styles.selected]
background = "dark_gray"
remove_modifiers = ["reversed"]
# Navigator resource levels default to red, blue, green, and magenta.
[ui.styles.session]
foreground = "red"
[ui.styles.workspace]
foreground = "blue"
[ui.styles.tab]
foreground = "green"
[ui.styles.pane]
foreground = "magenta"
[ui.styles.divider]
foreground = "dark_gray"
[ui.styles.attention]
foreground = "yellow"
add_modifiers = ["bold"]
[ui.styles.activity]
foreground = "light_cyan"
[ui.styles.added]
foreground = "green"
[ui.styles.deleted]
foreground = "red"
[ui.tab_bar]
position = "top" # "top" or "bottom"
left = [
{ segments = [{ token = "workspace.icon", suffix = " " }, { token = "workspace.name", max_width = 20 }], style = "muted", priority = 200 },
]
center = [
{ segments = [{ component = "tabs" }], priority = 100 },
]
right = [
{ segments = [{ token = "workspace.extension.run.status", inverted = true, pill = true }], priority = 220 },
{ segments = [{ token = "client.zoom", suffix = " " }], priority = 255 },
{ segments = [{ token = "client.help" }], style = "muted", priority = 0 },
]
[ui.tab_bar.item]
segments = [
{ text = " " },
{ token = "tab.index" },
{ token = "tab.name", prefix = " " },
{ token = "tab.closing", prefix = " " },
{ token = "tab.activity", prefix = " " },
{ text = " " },
]
[ui.sidebar.left]
width = 28
display = "expanded" # "expanded" or "minimized"
visibility = "automatic" # "visible", "automatic", or "hidden"
components = [
{ component = "workspaces", size = "fill", header = [{ token = "session.name", style = "current" }], footer = [{ token = "sidebar.status", style = "muted" }], row = { left = [{ text = " " }], body = [{ token = "workspace.index" }, { token = "workspace.name", prefix = " " }], right = [{ token = "workspace.tab_count" }, { token = "workspace.closing", prefix = " " }, { text = " " }], detail = [{ text = " " }, { token = "workspace.git_branch", style = "muted" }, { token = "workspace.git_added", prefix = " " }, { token = "workspace.git_deleted", prefix = " " }] } },
]
[ui.sidebar.right]
width = 28
display = "expanded"
visibility = "automatic"
components = [
{ component = "agents", size = "fill", scope = "session" },
]
An explicitly empty array hides that lane or format.
Local extensions
extensions is an explicit list of trusted directory paths, loaded in order.
Paths beginning with ~/ are resolved from your home directory, relative paths
are resolved from the directory containing config.toml, and absolute paths
continue to work unchanged. Fut appends packages enabled through its Fut-owned
local managed store; it never rewrites this array or any other part of
config.toml. The complete merged set is accepted or rejected atomically, and
references to extension tokens are validated with it. Fut does not discover or
download extensions.
See Extensions for managed install commands, trust boundaries,
hooks, tokens, limits, payloads, and examples. Third-party authors should use
the Extension authoring guide as the API contract.
The global file, a trusted project recipe, and each workspace’s
.fut/config.toml may configure a loaded extension with the same namespaced
table. Values layer global → trusted project → workspace:
[extension.run]
command = ["just", "run"]
signal = "STATUS:READY" # optional literal readiness marker
click = "owner" # optional "owner" (default) or "logs"
Workspace values recursively override trusted project and global defaults for
explicit commands invoked there. Unknown extension IDs and oversized data are
errors; the extension owns validation of its inner keys and values. Fut gives
extensions the trusted global/project layers separately; the bundled automatic
behaviors use only those values. Workspace-local values remain inert in the
bundled extensions until an explicit command is invoked. See the bundled run
extension for optional trusted auto_start and manual
run:restart, and the bundled wt extension for optional existing-worktree
discovery.
Bindings
Set ui.prefix to change the default ctrl-b prefix. Bindings are unique
suffixes after it. Set a built-in action or quoted extension command slug under
[ui.bindings]; see the complete defaults table
for action names. To run an action immediately without entering the prefix,
map the key to the action under [ui.hotkeys] instead. Direct hotkeys are
additional: the action’s prefixed binding continues to work.
The command palette also includes rename-session, rename-workspace,
rename-tab, kill-session, kill-workspace, and kill-tab. They have no
default bindings; configure the corresponding rename_* or close_* keys to
add shortcuts. kill-pane remains bound through close_pane by default.
A prefix, suffix, or hotkey may be one printable character, ctrl-a through
ctrl-z, space, enter, tab, esc, up, or down. Suffixes also accept
prefix, meaning the configured prefix key again. A hotkey cannot be the
configured prefix or share a key with another hotkey. Pause after the prefix to
see the effective bindings, or press prefix then : to search the command
palette.
An [extension_commands."EXTENSION:COMMAND"] table can set args to replace
the arguments supplied after that extension command’s manifest executable.
The arguments are direct strings with no shell interpolation. An empty array
passes no arguments, and unknown qualified command slugs are rejected.
Trusted commands
Each [trusted_commands.NAME] table requires title and an executable program, plus optional binding, hotkey, string array args, size, and activate_opened values. binding follows the prefix; hotkey runs the command directly. program supports ~ and ~/ home-directory expansion. size and activate_opened have the same semantics as an extension command; omitting size preserves the full-terminal surface, while activation defaults to false. Running a command opens a dashed frame containing a temporary PTY, inherits the focused pane process’s live working directory, and sends normal terminal input to the command. The frame names the command and identifies the temporary surface; when the process exits, Fut restores the previous panes, focus, and geometry. A bound trusted command may take a built-in’s default key, which unbinds that built-in unless it is explicitly rebound under ui.bindings. Explicit binding and hotkey collisions and duplicate command keys are rejected. Commands appear in the command palette; prefixed command bindings also appear in delayed which-key help. Configuration reload replaces them atomically.
Sidebars
Agents is a read-only projection of terminals with an explicit agent integration. Its scope is tab, workspace, session, or global. Tab, workspace, and session filters use fresh live focus ancestry when available and otherwise fall back to the selected IDs; global needs no focus anchor. Rows under any closing session, workspace, tab, or pane are omitted; screen detection alone does not add a row. Rows show idle, working, blocked, and daemon-wide unread-completed activity plus session/workspace/tab context. Enter navigates with the row’s typed pane ID; global safely permits cross-session destinations. The separate Notifications dialog remains the unread-attention surface.
Each sidebar width is independently 4 through 80 cells and includes its one-cell inner divider. Each tagged built-in entry uses component = "workspaces" or component = "agents" and has either size = "fill" or a positive fixed row count; each side may contain at most one fill and at most one Workspaces component. Left-drag either visible divider to resize only that side. A docked drag preserves at least 40 terminal columns after accounting for the other docked side; an open drawer’s divider is also draggable, while a hidden drawer is not. Dragged widths belong only to that attached client: they are not written to configuration, and both configured widths return on reattach or configuration reload. The active workspace is marked with a bullet.
Display, visibility, components, and relevance are independent per side. display = "expanded" uses that side’s configured width, while display = "minimized" uses a fixed six-cell rail. Minimized Workspaces and Agents rows keep stable numeric positions plus current/activity markers. Opening a minimized sidebar temporarily expands its drawer to the configured width; the rail itself is not draggable, but its open drawer remains resizable. Press m inside a component to toggle its side. visibility = "visible" docks when geometry permits. visibility = "automatic" docks when that side has a relevant configured component: Workspaces with multiple live workspaces, or Agents with an integrated terminal in their configured scope. visibility = "hidden" leaves the side as an on-demand drawer. Press h to cycle that side through visible → automatic → hidden. With the default Workspaces footer, an open sidebar shows h, m, and ? on separate lines with their current states; the nerd_font preset adds matching visibility, width, and help icons.
The defaults are left Automatic with one Workspaces fill, and right Automatic with one session-scoped Agents fill; an empty Agents projection therefore consumes no right-side width. Both sides dock when their combined widths leave 40 terminal columns. If only one can fit, allocation is deterministic: left is considered before right. Any hidden, irrelevant, or non-fitting side remains available as its own edge drawer without reducing terminal geometry. The current side’s display and visibility appear in the default footer. sidebar.display and sidebar.visibility expose those labels to custom sidebar chrome. vertical_divider must be exactly one grapheme and one display cell.
Segments, groups, and components
Every segment sets exactly one of:
text— a literal string;token— a pure value from Fut’s already-materialized client/resource state;component— a layout-aware repeated collection. The only current component istabs.
Token segments may also set prefix, suffix, max_width, style, inverted = true, and pill = true. Prefix and suffix are emitted only when the token is nonempty. Inversion uses terminal reverse-video after normal style composition: the semantic foreground becomes the fill and the glyph uses the underlying terminal background, so it adapts to light and dark themes. A pill requires inversion and adds the configured pill_left and pill_right glyphs in that fill color around the complete segment, including its affixes. If either cap is empty, as in the Unicode and ASCII presets, Fut renders only the inverted content. Empty tokens emit neither affixes nor caps. Text segments accept style but not token-only options. Components must be the only segment in their group and do not accept segment options.
Tab-item content uses the intrinsic display-cell width of its configured segments. The default format supplies one cell of padding on each side; add or remove text segments to adjust that spacing. The Nerd Font preset additionally reserves one cell at each end of every item so its active-tab pill does not move neighboring tabs. New tabs are created unnamed, so their shared title follows the foreground process in the oldest surviving pane. Focusing another pane does not rename the tab. Name a tab with fut tab new --name or Ctrl-b r to keep that title fixed; renaming it to an empty string restores automatic naming.
Tab-bar lanes contain groups. A group has segments, an optional semantic style, and a priority from 0 through 255. The tabs component is flexible and keeps its active or keyboard-selected item visible. Groups with higher priority than tabs reserve their complete width first. Tabs then grow toward their complete preferred width. Lower-priority groups appear only in remaining space. Left and right lanes stay edge-aligned; the center lane is geometrically centered and clamped between them. Groups never overlap. By default the right lane names the current workspace, truncated at 20 cells.
Workspace rows have intrinsic left and right lanes; body receives the remaining cells and truncates safely. Expanded default rows show the workspace number and name, reserve leading and trailing padding, and use the same current style as the focused tab instead of a separate active marker. With Nerd Font pill caps, only that title line becomes a pill. The minimized rail keeps its compact active-workspace bullet. A nonempty detail format adds a second full-width line. Workspaces are unnamed unless explicitly named, presenting as their live location — the work tree (or directory) every open pane is inside, or multiple when panes disagree. The default detail aligns under the row name and shows the Git branch at that live location with its short working-tree diff (+N inserted, -N deleted). The daemon collects those values with bounded background git processes and publishes them atomically into the shared resource snapshot, so rendering never waits, attached clients agree, and non-Git locations simply stay empty. Set detail = [] for compact one-line rows. Header and footer are optional single-line segment lists. At tiny heights Fut preserves resource rows over decorative header/footer content, while switching and error status remains visible.
All widths are terminal display cells. Dynamic values are sanitized and truncated at grapheme boundaries. Bars never wrap.
Spinners and effects
Run fut --ui-playground to browse the built-in non-emoji spinner styles at
their intended speed and preview plain, pulse, wave, inverted, and pill token
treatments—including effect combinations—with the active theme. The standalone
playground reads the normal configuration but does not start or contact a
daemon. Search by typing and move with the arrow keys or Ctrl-j/Ctrl-k.
Choose a named style or provide a custom, equal-width frame list:
[ui.spinner]
style = "circle_halves"
# interval = 80 # Override the preset's frame delay.
# Or define a custom spinner. `style` is ignored when frames are present.
# frames = ["-", "\\", "|", "/"]
# interval = 100
The interval must be 16–2000 milliseconds. Custom lists accept 1–256 safe,
nonempty frames, each 1–32 display cells wide; every frame must have the same
display width so surrounding UI does not jump. The selected spinner is shared
by agent activity and extension tokens declared with presentation = "spinner".
Pulse alternates the complete text between normal and faint. Wave moves the faint treatment across its characters. Neither effect changes glyph weight; affixes participate while pill caps retain a stable fill.
Styles
The fixed semantic roles are:
normalmutedsessionworkspacetabpanecurrentselectedclosingactivityattentionerrordivideraddeddeleted
Each style accepts:
foreground = "yellow"
background = "default"
add_modifiers = ["bold", "underlined"]
remove_modifiers = ["dim"]
Modifiers are bold, dim, italic, underlined, reversed, and crossed_out.
Colors may be default, ANSI names such as red, blue, gray, dark_gray, or light_cyan, an indexed color such as index:123, or exact RGB such as #12abef. Indexed colors remain references to the containing terminal’s palette; RGB colors remain exact.
Styles compose in this order: normal, group style, token style (activity, attention, added, or deleted when supplied), segment style, current, attention, closing, selected. Later foreground/background values replace earlier ones; modifiers are added or removed in sequence. An inverted token adds reverse-video after this composition without discarding the composed colors or modifiers.
Icons
unicode is the default and requires no private-use glyphs. ascii uses ASCII for the configurable resource icons; ordinary built-in help and truncation text may still use Unicode. nerd_font opts into a small Nerd Fonts v3-oriented resource/state set, and additionally uses its Powerline half-circle caps for focused tabs, expanded workspaces, and token segments configured with pill = true. Every icon can be overridden under [ui.icons].
Fut cannot reliably detect the active terminal font. Use fut doctor for an honest visual probe; it never claims that an installed font is active.
Security boundary
Everything under ui remains non-executable: it has no shell commands, file reads, environment interpolation, networking, functions, or expression language, and presentation tokens resolve only daemon-validated materialized values and actions. trusted_commands is a deliberate executable trust boundary. Fut executes its program directly with the configured args and the client’s environment; it does not invoke a shell unless you explicitly configure one. Explicit extension roots, enabled managed extensions, and namespaced project tables are the equivalent trust decision for extension manifests, their packaged commands, token click actions, and values those commands may execute. Managed installation only copies a local package or fetches one explicitly pinned Git commit before validating it; it runs no package scripts, and enablement is the explicit trust decision. An explicit project recipe path is also a global trust decision; a repository .fut/project.toml becomes executable only after fut trust [PATH] approves that canonical file’s exact current bytes. Only configure commands, extension directories, and recipes you trust. Commands never run during configuration parsing or rendering; token commands require an explicit click, resource hooks run only from committed daemon mutations, client hooks run only for attachment lifecycle transitions, and recipe tabs, panes, and commands run only while bootstrapping a project session. Trusted recipe extension settings may deliberately make lifecycle hooks perform work such as opening existing worktrees or starting a managed command when the project session is created. The same-user Unix socket is the authorization boundary for token publication, just as it is for other Fut control commands; Fut does not claim to authenticate the publishing process beyond that boundary.