Skip to content

User config

Roost reads a single user-level config file at ~/.config/roost/config.conf (XDG-style on macOS by deliberate divergence from Apple HIG — matching Ghostty / nvim / fish). Both UIs — the Swift Mac app and the iced binary Linux installs as roost — parse the same file with the same semantics, so a config tuned on one platform is portable to the other. A few settings are honored by only one of them — where that is the case, the setting's own section says so explicitly (see link-modifier); unknown and unsupported keys are dropped rather than erroring, so a portable file stays valid everywhere.

The file is plain text, one key = value per line, #-prefixed comments allowed, whitespace forgiving. Unknown keys are silently dropped — this keeps user files forward-compatible with future Roost versions and with keys that Ghostty consumes but Roost doesn't.

A missing file is fine: every setting has a compiled-in default.

To override the path for testing, set the ROOST_CONFIG environment variable to an absolute file path. The E2E harness uses this to seed the launcher with deterministic commands.

Settings

Key Type Default Effect
theme string bundled roost-dark Theme name (see themes.md).
font-family string system monospace (Swift Mac app) / JetBrains Mono, Monospace (iced UI, which bundles JetBrains Mono) Monospaced font family. Quoted values supported ("JetBrains Mono"). See Fonts.
word-break-chars string `_-.+~/:@%` Extra characters treated as word characters for double-click word selection (keeps paths + URLs whole). Despite the -break- name (kept for Ghostty compatibility), the value is the extra word-char set.
show-sidebar-agents bool true Whether the sidebar shows the Agents panel — every agent on every host in one list below the projects — and the agent count on each project row. The Swift app shows one row per agent-owned tab under its project instead. Also toggled at runtime (keybind / palette / Mac View menu).
agent-panel-sort grouped \| priority \| recent grouped How the sidebar's Agents panel orders its rows: in sidebar order under a head per host and project, most urgent first, or most recent first. Also cycled at runtime by the cycle_agent_panel_sort action and the palette's Cycle Agent Panel Sort, which write the choice back here. An unrecognized value logs a warning and reads as grouped. iced-only.
font-size number 13 (Linux), 14 (Mac) Point size for the terminal font. Must be > 0. One point is one pixel on macOS; Linux renders at 96 dpi (13 ≈ 17.3 px). See Fonts.
tab-min-width number 80 (Mac) Minimum tab pill width in points. 0 disables the floor. Mac-only.
tab-max-width number 220 (Mac) Maximum tab pill width in points. 0 disables the cap (pills grow to fit). Mac-only.
keybind <trigger> = <action> (see Keybindings) Append a custom keybinding. Repeatable; later entries override earlier ones.
command label="…" run="…" [hold=…] none Launcher entry (⌘⇧T / Alt+Shift+T) that runs a fixed command in a new tab. Repeatable. See Extending Roost.
provider label="…" run="…" [timeout=…] [limit=…] none Dynamic, script-backed menu in the custom palette (⌘⇧E / Alt+Shift+E). The script generates rows on demand and acts on the choice. Repeatable; executables in providers/ (beside this file) are also discovered. See Extending Roost.
copy-on-select off \| true \| clipboard true What a mouse-drag selection writes to the clipboard on release. See the dedicated section below.
clipboard-write allow \| deny allow Whether a program running in the terminal can write the host clipboard via OSC 52. See the dedicated section below.
link-modifier ctrl \| alt \| super Cmd (Mac) / Alt (Linux) Which held modifier reveals + opens a URL on hover/click. iced-only; the Swift Mac app is fixed to Cmd. See the dedicated section below.
macos-option-as-alt false \| true \| left \| right false Which Option keys send Alt (Meta) on macOS, so ⌥B sends ESC b instead of typing ∫. Read by the iced UI on macOS only. See the dedicated section below.
macos-auto-secure-input bool true Turn Secure Keyboard Entry on while the active tab is at a password prompt. Read by the iced UI on macOS only. See Secure Keyboard Entry.
macos-secure-input-indication bool true Show a lock at the right end of the tab band while Secure Keyboard Entry is on. Read by the iced UI on macOS only. See Secure Keyboard Entry.
macos-secure-keyboard-entry bool false Keep Secure Keyboard Entry on whenever Roost is the active app. Written by the menu's Secure Keyboard Entry row, the palette's Toggle Secure Keyboard Entry and the toggle_secure_input action. Read by the iced UI on macOS only. See Secure Keyboard Entry.
agent-hooks agent list | off | absent absent (unanswered) Which supported coding agents (Claude Code, Codex, grok/gx, cursor-agent, OpenCode) Roost wires its hook entries into, at startup. Absent means nobody has answered the consent dialog yet — Roost writes nothing until they do. See the dedicated section below and the Agent Hooks guide.
local-backend in-process \| session in-process for an existing setup; session on a genuinely fresh install Where the tabs you start in the Roost window run. See the dedicated section below.
chrome-accent system \| #rrggbb system The accent color of the window chrome: the active tab and project, notification dots, focus rings. iced-only. See the dedicated section below.

copy-on-select

Controls what happens to a text selection the moment the user releases the mouse after a drag. Vocabulary matches Ghostty 1:1 — three values with platform-specific behaviors:

Value What it writes on drag-release What pastes from there
off nothing n/a — user must press the explicit copy shortcut (⌘C on Mac, Ctrl+Shift+C on Linux)
true (default) the selection clipboard only middle-click anywhere in Roost; ⌘V / Ctrl+Shift+V are not affected
clipboard both the selection clipboard and the system clipboard middle-click (selection); ⌘V / Ctrl+Shift+V (system)

Per-platform details

Linux. The selection clipboard is the X11 / Wayland PRIMARY selection — the conventional one-finger-middle-click target. The system clipboard is CLIPBOARD (the Ctrl+C / Ctrl+V target most non-terminal apps use). true matches the long-standing X11 convention where dragging in a terminal writes PRIMARY but leaves CLIPBOARD untouched.

macOS. Mac has no native equivalent of PRIMARY, so Roost synthesizes one as a custom-named NSPasteboard (ai.stridelabs.Roost.selection). Other Mac apps cannot read it — it exists purely so Roost can offer middle-click paste without clobbering the system pasteboard that ⌘V reads from. Practical consequence: with the default copy-on-select = true, dragging a selection in Roost and then pressing ⌘V in another app will paste whatever was last ⌘C'd, not the dragged text. This surprises Mac users who expect drag to update the system clipboard. To get the "drag and paste anywhere" behavior, set copy-on-select = clipboard.

Middle-click paste

Middle-click anywhere in a Roost terminal pastes from the selection clipboard, regardless of which copy-on-select value is set. The gesture works on both platforms even when copy-on-select = off; in that case the selection clipboard is just empty (whatever the user last explicitly wrote via the copy shortcut isn't in there), so the paste is a no-op.

Paste contents are routed through the same bracketed-paste-aware encoder as ⌘V / Ctrl+Shift+V, so an nvim or fish that enables DECSET 2004 receives ESC[200~ … ESC[201~ wrappers around the selection.

Soft-wrapped lines

A line too long for the window is soft-wrapped: the terminal breaks it across several screen rows purely to fit, with no newline in the data. Copying rejoins those rows, so a wrapped line comes back as the one long line it always was — the same thing Ghostty does. Paste it into an editor and you get one line, not one per screen row, and a word split across the break comes back whole.

Only the wraps the terminal made are absorbed. A newline the program actually wrote still breaks the copy, and a wide (CJK / emoji) glyph that had to move to the next row to fit copies once, in the right place.

Trimming

The text written to either clipboard has trailing spaces stripped per line (matches Ghostty's clipboard-trim-trailing-spaces default behavior). Only U+0020 is stripped — any other whitespace codepoint in a cell is content the program deliberately wrote. "Line" means the rejoined line, so spaces sitting at a soft-wrap boundary are interior and survive; trimming them would glue two words together. Entirely-blank trailing rows are dropped so a multi-row selection doesn't carry stray newlines; leading and interior blank rows are kept, since they are part of what was selected.

A selection that extends beyond the visible viewport copies in full, including rows scrolled off-screen (#249). Endpoints are tracked pins into the terminal's own storage (#334), so a selection follows its content: it stays on the same characters as history scrolls past, as the buffer drops rows off the top, and across a window resize that reflows the line. A row that is evicted from scrollback entirely takes the selection with it — copy then returns nothing, rather than quietly handing back whichever row moved into that slot.

A selection also belongs to the screen it was made on. Start an alt-screen program (vim, less, htop) and a selection made in the normal scrollback stops being drawn and copies nothing; it reappears when the program exits.

clipboard-write

Controls whether a program running inside the terminal can write the host clipboard by emitting the OSC 52 escape sequence (\e]52;c;<base64>\a). This is the path opencode-over-SSH, nvim with g:clipboard = osc52, tmux set -s set-clipboard on, kitten ssh, yazi, and other TUIs use to get text back to your local clipboard.

Value Behavior
allow (default) OSC 52 writes the host clipboard. Matches Ghostty's default.
deny OSC 52 sequences are parsed and silently dropped — logged at info, no clipboard side-effect.

Phase 2 will add ask with a per-tab consent banner ("opencode wants to write 42 bytes to your clipboard — Allow once / Always / Deny"); phase 1 is intentionally allow/deny only to keep the surface small.

Read direction (OSC 52 ?)

OSC 52 also supports a read direction (the program asks the terminal to send the clipboard contents back). Roost always drops read requests in phase 1 — there's no consent UI for them yet and reading the clipboard from a remote process is the more sensitive direction (shoulder-surfing a password manager value). This will become its own clipboard-read = allow | ask | deny setting in phase 2.

This posture is unconditional across a host session's SSH transport too: a read request is parsed and dropped identically whether the terminal it arrived on is local or attached over SSH to a remote host — reading is more sensitive, not less, when the process asking runs on a machine you don't sit at, so there's no separate carve-out for it. A write from a program on a remote host still lands on this machine's clipboard, subject to the same clipboard-write setting above — the session forwards the OSC 52 write to whichever client currently holds the tab, and that client's own clipboard-write config decides whether it's applied.

Targets

OSC 52 carries a Ps selector indicating which clipboard to write:

  • c (default) → system clipboard (NSPasteboard.general on Mac, CLIPBOARD on Linux — what ⌘V / Ctrl+V pastes from).
  • p or s → selection clipboard (named NSPasteboard on Mac, X11 / Wayland PRIMARY on Linux — what middle-click pastes from).
  • Any other selector falls through to system (matches Ghostty's permissive handling of emitters that pad the selector with letters).

Holding this modifier while hovering a URL reveals it (underline + hand cursor); holding it while clicking opens it in your default browser. URLs come from both OSC 8 hyperlinks (what tools like Claude Code emit) and plain https://… text matched on screen.

Value Modifier
ctrl Control
alt Alt / Option
super (aliases: cmd, command, meta) Super / Command

Defaults are platform-native, mirroring the keybinding scheme:

  • macOS: Cmd — matches the Swift app and native Mac apps (⌘-click).
  • Linux: Alt — Roost's single "primary" modifier on Linux, leaving Ctrl free for the shell/readline.

Linux users who prefer the traditional Ctrl+click (the ghostty / common-terminal convention) just set:

link-modifier = ctrl

Scope: this setting is honored by the iced UI — the shipped Linux roost and the experimental Roost-Iced.app alike (link_modifier_held in crates/roost-iced/src/app/interactions.rs). The Swift Mac app's modifier is currently fixed to Cmd, so the key is silently ignored there (harmless — unknown keys are always dropped).

Heads up (Linux): some window managers/compositors grab Alt+drag to move windows, which can swallow Alt+click. If link-clicking feels flaky on your desktop, switch to link-modifier = ctrl.

Prefer Ctrl across the board? (Linux)

On Linux Roost defaults to an Alt-centric scheme (Alt+T new tab, Alt+W close, etc.) so Ctrl stays free for the shell — the only Ctrl defaults are Ctrl+1…Ctrl+9 (tab switching) and Ctrl+Shift+C/V. If you'd rather use the more familiar Ctrl shortcuts, you can remap both the link modifier and any keybinding — keybind lines are repeatable and last-wins:

# Ctrl+click to open links
link-modifier = ctrl

# Restore Ctrl-based shortcuts (examples — see the keybindings doc)
keybind = ctrl+t = new_tab
keybind = ctrl+w = close_tab

See Keybindings for the full action list and trigger syntax.

macos-option-as-alt

On macOS, Option is a text modifier: ⌥B types ∫ and ⌥F types ƒ. Shells and editors that use Meta chords, such as readline's ⌥B and ⌥F word jumps, want those keys to send ESC b and ESC f instead. This setting makes Option act as Alt. The key and its values are Ghostty's.

Value Which Option sends Alt
false (default) neither; both type macOS characters
true both
left the left Option; the right one still types characters
right the right Option; the left one still types characters

Values are case-insensitive. An unrecognized value logs a warning and reads as false.

macos-option-as-alt = left

When Option acts as Alt, it sends the key without Option's character: ⌥B sends ESC b, and ⇧⌥B sends ESC B. Two kinds of key still type the character macOS gives them:

  • a shifted key that is not a letter, such as ⇧⌥1, because its shifted character depends on the keyboard layout;
  • a key whose own character is not ASCII, such as é on a French layout, because the Meta prefix carries a single byte.

Limitations:

  • The dead-key chords ⌥E, ⌥U, ⌥N, ⌥I and ⌥` are taken by macOS's input method before Roost sees them, so they still start an accent (#582).
  • left and right tell the two Options apart by the Option key's own press and release. If Roost has seen neither while Option is held, it counts that Option as the configured side, and logs this once.

Scope: read by the iced UI on macOS (Roost-Iced.app). On Linux, Alt already sends the ESC prefix, so the key is parsed and ignored. The Swift Mac app ignores it too (unknown keys are always dropped).

Secure Keyboard Entry

While Secure Keyboard Entry is on, macOS stops other processes — event taps, keyloggers, some accessibility tools — from reading what you type. Terminal.app, iTerm2 and Ghostty all offer it. Roost follows Ghostty's model, and uses Ghostty's key names:

Key Default Meaning
macos-auto-secure-input true Turn it on while the active tab is at a password prompt.
macos-secure-input-indication true Show the lock while it is on.
macos-secure-keyboard-entry false Keep it on whenever Roost is the active app.
macos-auto-secure-input = true
macos-secure-keyboard-entry = false

Secure Keyboard Entry is on while Roost is the active app and either macos-secure-keyboard-entry is on, or macos-auto-secure-input is on and the active tab is at a password prompt. Switching to another app turns it off at once, including while one of Roost's menus is open, and coming back turns it back on. Quitting Roost always gives it back.

The toggle. The app menu's Secure Keyboard Entry row, just below Settings…, shows a checkmark while macos-secure-keyboard-entry is on and flips it. The command palette's Toggle Secure Keyboard Entry row and the toggle_secure_input action do the same; the action has no default shortcut (see Keybindings). The choice is written back to config.conf, so it survives a restart.

The lock. While it is on, a small lock sits at the right end of the tab band; hover it for an explanation. macos-secure-input-indication = false hides it.

How a password prompt is recognised. A tab is at a password prompt while its terminal is in line mode with echo off — what sudo, ssh's own password prompt, passwd and read -s set. Roost checks every 200 ms. Only the active tab counts: a prompt in a background tab turns nothing on until you switch to it. Any program that turns echo off in line mode is treated as a prompt; set macos-auto-secure-input = false if that gets in the way, for example of accessibility software.

Remote prompts. A host-session tab, whose shell runs in a roost-session on another machine, does catch a remote sudo: the session watches its own terminals and reports a prompt to Roost. That needs a roost-session new enough to report it; an older one never does, and its tabs never turn Secure Keyboard Entry on by themselves. A plain ssh run in a local tab is different: ssh puts the local terminal in raw mode, so a password prompt on the far side is never seen. Turn on macos-secure-keyboard-entry for those.

Scope: the iced UI on macOS (Roost-Iced.app). On Linux the three keys are parsed and ignored, toggle_secure_input does nothing, and there is no menu row, palette row or lock. The Swift Mac app ignores the keys too (unknown keys are always dropped).

agent-hooks

Which supported coding agents Roost wires its hook entries into, in their own config files. Both UIs read this at startup, and roostctl agent ensure (the same verb the UIs run) reads it too — see the Agent Hooks guide for the full mechanism, what gets written where, and the guarantee about what a merge does and doesn't touch.

agent-hooks = claude, codex   # a list, off, or absent (nobody has answered yet)
Value Effect
a comma list of agent names Every named agent gets Roost's hook entries, refreshed on launch if they're stale. Everything present and not named is left alone — nothing is ever removed just for being absent from the list.
off — also accepts false, no The UIs wire nothing at startup; an agent's config file is never opened. This does not remove anything already wired on its own — see below.
absent, empty, or unrecognized Unanswered. Nobody has run the consent dialog or roostctl agent set yet. Nothing is wired, and nothing already wired is touched, until they do — a fresh install starts here, and a launch raises the consent dialog once per process when at least one supported agent is installed. See Agent Hooks → Roost asks once.

Names normalise. Each token is trimmed and lowercased; duplicates collapse; the result is reordered into claude, codex, grok, cursor, opencode regardless of the order written. A token that names none of the five wires nothing and warns, and the rest of the list still resolves — one typo shouldn't turn into "nothing is wired and nothing says why." It is kept in the key, though, and written back unchanged: a newer Roost that supports more agents writes its name here, and an older one that erased it on the next launch would silently undo that answer. roostctl agent status lists it as unknown to this build. A value that names no recognised agent at all, or mixes a reserved word (off/false/no, or the retired auto/on/true/ yes) with anything else, resolves to unanswered with a warning — the retired spellings no longer mean "wire everything"; they mean "nobody has said yet." A repeated key is last-wins, including when the repeat is empty or unparseable (it resolves to unanswered, not to whatever an earlier line in the file said).

off stops future wiring; it does not itself remove anything. This split is deliberate, not an oversight: flipping the key to off and relaunching means the UI won't wire anything new, but entries already on disk stay there until something explicit takes them out — roostctl agent uninstall --all, running roostctl agent ensure yourself (which reads this same key and, seeing off, does perform the removal), or reopening the consent dialog (the Agent Hooks… palette row) and choosing off there. The UIs themselves never rewrite an agent's config file just because the key changed to off — they simply stop touching it.

Over a connected host session the rule is different again, and reversed from an earlier design. A connecting client can only ever raise a host's agent-hooks key — union its own allow-list into whatever the host's key already says, never removing — and that includes raising a host whose key is explicitly off or still unanswered: a host has no screen to ask on, so the connecting client is the only authority there is. A client whose own key is off or unanswered sends nothing and so cannot raise or lower a host either. Lowering a host is done on that machine — roostctl agent ensure/uninstall run there, or its own dialog — and holds until a more permissive client connects again. See Agent Hooks → Remote hosts for the full behavior.

The Mac app carries its own mirror of this parser (AgentHooks in Config.swift) rather than reading it only through roostctl — it needs the key at launch to decide whether to raise the consent sheet before it has spawned anything — kept in parity with the Rust parser, including the normalisation rules, by ConfigAgentHooksTests.

local-backend

Where the tabs you start in the Roost window run: in the UI process (in-process, they end when Roost quits), or in a local host session (session, they keep running, and any of your own clients can attach). See the Switching the local backend guide section for the two palette rows that move between them live, with no config editing or relaunch required.

local-backend = in-process   # in-process (existing setups) | session (fresh installs)

The default depends on what's already on disk, not on the platform. A genuinely fresh install — no state.json and no config.conf anywhere Roost would look — starts on session from its very first launch, and Roost writes the key to config.conf for you so the choice is visible and stays put. Any machine that already has either file keeps in-process, exactly as it always has, until you switch it yourself. This asymmetry is deliberate: only a machine with nothing of this app on it yet is a machine where "session" isn't a behavior change to a saved layout.

An unrecognized value (a typo, an older config surviving a downgrade) logs a warning and reads as in-process — configured badly, not unconfigured, so it never triggers the fresh-install write on a later launch.

Read by the iced UI. The Swift Mac app (Roost.app) is always in-process and never reads this key.

chrome-accent

The accent color of the window chrome around the terminal: the active tab pill and project row, the notification dots, the outline of a dragged tab, the focus ring, the confirm button and the command palette's match highlight.

Value Accent
system (default) macOS: the system accent color (System Settings → Appearance), followed live when you change it. Linux: Roost's blue, #007aff — the desktop's accent is not followed.
#rrggbb That color, on both platforms. Hex digits in either case.

Under Roost's blue the chrome keeps its hand-picked fills. Any other accent derives them the way macOS's own controls do: the active tab is the accent at 18% over the tab band, and the selected project row is the accent mixed half and half with a dark grey.

Anything else (a color name, a short hex) logs a warning and reads as system.

Read by the iced UI. The Swift Mac app (Roost.app) ignores this key and always follows the system accent.

Example

# ~/.config/roost/config.conf

theme = Catppuccin Mocha
font-family = "JetBrains Mono"
font-size = 14

# Drag-to-select writes to PRIMARY (Linux) / Roost's named selection
# pasteboard (Mac). Middle-click pastes. Cmd-V / Ctrl+Shift+V are
# untouched.
copy-on-select = true

# To make drag-to-select also write the system clipboard, so Cmd-V
# in another Mac app gets the dragged text:
#
#   copy-on-select = clipboard

# Default: programs in the terminal can write your clipboard via
# OSC 52 (the opencode-over-SSH path, nvim's g:clipboard = osc52,
# tmux set-clipboard, etc.). Set `deny` to opt out.
clipboard-write = allow

# Open links with Ctrl+click instead of the platform default
# (Cmd on Mac, Alt on Linux). iced UI only — the Swift Mac app is
# always Cmd-click.
link-modifier = ctrl

# Add a custom trigger (here, restoring the pre-Alt Ctrl+T for new_tab
# on Linux — the default is now Alt+T). See docs/getting-started/keybindings.md.
keybind = ctrl+t = new_tab

# A fixed launcher command (⌘⇧T / Alt+Shift+T):
command = label="Claude" run="claude --resume"

# A dynamic, script-backed menu (⌘⇧E / Alt+Shift+E). The script prints
# its rows on `list` and acts on the choice on `activate`:
provider = label="Open shed" run="~/.config/roost/providers/shed.sh"

# Left absent (the default), Roost asks once — a consent dialog opens
# on first launch naming whichever of Claude Code, Codex, grok/gx,
# cursor-agent and OpenCode it finds installed, and nothing is wired
# until you answer it. Answering by hand instead: name the agents to
# wire, or turn the whole thing off (which stops future wiring — it
# doesn't by itself remove what's already there; see
# docs/guides/agents.md).
agent-hooks = claude, codex

See Extending Roost for the full command = / provider = contract (with bash / Python / TypeScript examples), paths.md for where the file lives on each platform, themes.md for the theme value enumeration, and the Agent Hooks guide for agent-hooks in full.