Configuration

Every knob Thurbox reads, where it lives, and how it behaves. One file per audience/lifecycle: hand-edited registries are TOML, the machine-written keybindings are JSON, and concurrently-written runtime state lives in SQLite.

Dev builds (version 0.0.0-dev ) use thurbox-dev in place of thurbox in every path below, plus a thurbox-dev tmux socket, so a development checkout never touches your real setup. All paths respect $XDG_CONFIG_HOME / $XDG_DATA_HOME .

Files at a glance #

File Format Edited by Read Purpose
~/.config/thurbox/agents.toml TOML you live (mtime poll) coding-agent CLI definitions
~/.config/thurbox/hosts.toml TOML you startup remote SSH hosts
~/.config/thurbox/settings.toml TOML you / Settings panel live (mtime poll) tuning knobs + feature flags
~/.config/thurbox/themes.toml TOML you startup custom theme palettes
~/.config/thurbox/hooks.toml TOML you on each session operation session lifecycle hooks — your commands, run before/after a session is created, deleted, restarted or restored
~/.config/thurbox/ui/ Lua you live (watched, 120 ms debounce; F10 forces) the interface itself — one file per pane, plus layout.lua and lib/
~/.config/thurbox/ui/plugins.toml TOML you (or thurbox-cli plugin) on each plugin command what the interface is composed of — a source, a destination file and an optional pin, per installed pane
~/.config/thurbox/ui/plugins.lock TOML thurbox-cli plugin on each plugin command what each entry resolved to, and the digest of every file delivered. Machine-written — commit it beside the spec and the same interface reproduces elsewhere
~/.config/thurbox/ui.json JSON F1 / Interface tab (or you) startup your decisions about the interface: rebound chords, plugins turned off, files trusted, plugin settings
~/.config/thurbox/keybindings.json JSON boot (migration only) the v1 chord-override file, kept so v1 rebindings still migrate. Read at boot only when ui.json carries no bindings ; the six v1 action names it recognises become ui.json overrides. New rebindings go to ui.json .
~/.config/thurbox/extensions/<name>.toml TOML extension install startup + tick extension manifests (self-healed resources)
~/.local/share/thurbox/thurbox.db SQLite thurbox live sessions, automations, tasks, theme, editor command
~/.local/share/thurbox/thurbox.log text thurbox logs (incl. config warnings)

settings.toml reloads live : the TUI polls its mtime (~1/s) and applies edits with a confirmation toast — no restart. The shell_pane/perf_hud/soft_delete feature flags apply immediately; the [notifications] knobs, the automations/mouse/notifications/version_check/auto_update feature flags, and the write-once scalars take effect on the next launch and the toast says so. A changed agents.toml entry applies to the next session you create; a new agent, and any change to hosts.toml or themes.toml , needs a restart.

Config problems land in the log file. Unknown TOML keys are tolerated and reported by name, while syntax/type errors fall back to built-ins (agents), zero hosts, or defaults (settings). Check everything from the command line:

bash
thurbox-cli config validate   # strict parse of every file; exit 1 on problems
thurbox-cli config show       # effective config + where each value came from

Which file do I edit?

A task-to-file map so you don't have to scan every section to find the right knob. None of these files need to exist on a fresh install — every one is seeded (commented-out where applicable) on first run, and absent files fall back to built-in defaults.

I want to… Edit
Add a coding agent, pin a model, change resume/fork flags agents.toml
Run sessions on a remote machine over SSH hosts.toml
Turn a whole TUI feature on/off (tasks, mouse, notifications…) settings.toml [features]
Tune scrollback, panel breakpoints, audit retention settings.toml
Change when/how OS notifications fire settings.toml [notifications]
Add or recolour a TUI theme themes.toml
Run my own command when a session is created, deleted, restarted or restored — or refuse one hooks.toml
Rebind a key the F1 editor (persisted to ui.json)
Set the Ctrl+O editor, pick a theme runtime — SQLite

agents.toml #

Declares the launchable coding agents. Seeded with the built-ins ( claude , codex , antigravity , opencode , aider , copilot , vibe , pi , omp ) on first run; edit or add [[agents]] entries to support any CLI — no recompile. A malformed file falls back to the built-ins and shows the error.

agents.toml
config_version = 1
default = "claude"          # agent preselected in the picker / headless spawns

[[agents]]
name = "claude"             # display + lookup name (unique)
command = "claude"          # executable
args = []                   # always passed; bake a model here if you want one
resume_args = ["--resume", "{id}"]            # emitted when resuming
fork_args = ["--resume", "{id}", "--fork-session"]
new_session_args = ["--session-id", "{id}"]   # emitted on a fresh spawn
resume_latest = false       # true = id-less "resume last session in cwd"

{id} is substituted with the thurbox-generated session UUID. Groups are emitted only when their driving value exists; precedence is fork > resume > new-session.

For each built-in's recommended configuration and runtime behavior — resume/fork semantics, whether it pins a session id, and how it reports status — see Built-in agents.

hosts.toml #

Declares remote SSH hosts; each [[hosts]] entry registers a session backend named ssh:<name> . Seeded fully commented-out (fresh installs are local-only). A malformed file means zero remote hosts and shows the error.

Field Required Default Purpose
name yes backend id ssh:<name>; what --host expects
destination yes ssh target (user@host or ~/.ssh/config alias)
ssh_opts no [] extra ssh flags, one token per element
socket no thurbox remote tmux -L socket
session no thurbox remote tmux session name
worktrees_dir no remote $HOME/.local/share/thurbox/worktrees absolute remote worktrees dir

Auth comes entirely from your ~/.ssh/config ; thurbox never handles credentials. Host changes require a restart.

hooks.toml #

Declares session lifecycle hooks : your own shell commands, run by thurbox before and after it creates, deletes, restarts or restores a session. Seeded fully commented-out (a fresh install runs nothing) and read each time an event fires , so an edit is in force at the next operation with no restart. A malformed file means no hooks run, a warning in the log, and a failing config validate .

Not the hooks/ directory beside it. That is the home of the built-in agent hooks extension , which installs status-hook files into the agent CLIs so they can tell thurbox what they are doing. hooks.toml is the other direction: thurbox telling your scripts what it is doing.

hooks.toml
[[hooks]]
event = "session.pre_create"
command = 'case "$THURBOX_BRANCH" in main|master) echo "refusing: protected branch" >&2; exit 1;; esac'

[[hooks]]
event = "session.post_create"
command = '[ -n "$THURBOX_CWD" ] && cp -n .env.local "$THURBOX_CWD/.env"; true'
timeout_secs = 120
Field Required Default Purpose
event yes one of the eight events below
command yes run through sh -c (cmd /C on Windows)
timeout_secs no 30 the hook is killed after this long

A pre_* event fires before the operation has any side effect; a post_* event fires after it has fully succeeded, and never after a failure. Hooks fire once per operation whichever interface asked — the TUI, thurbox-cli , an automation, an extension — because every interface ends in the same pipeline. Hooks for one event run one at a time, in file order.

Operation Events Fired by
create (incl. fork) session.pre_create / session.post_create the creation flow, Ctrl+F, thurbox-cli session create, a spawn automation, an extension's sessions
delete (soft or force) session.pre_delete / session.post_delete Ctrl+D, thurbox-cli session delete [--force], extension uninstall
restart session.pre_restart / session.post_restart Ctrl+R, thurbox-cli session restart
restore (incl. undo) session.pre_restore / session.post_restore Ctrl+Z/Ctrl+U, thurbox-cli session restore

A pre_* hook can refuse. Exit non-zero (or exceed the timeout) and the operation is aborted before it has done anything — no worktree, no process, no row changed — with the hook's command, exit status and the tail of its stderr as the reported reason (the in-flight error in the TUI; the error and exit status of thurbox-cli ). Later hooks for that event do not run. A post_* hook is informational : every one runs, a failure is logged to thurbox.log and carried in the CLI's JSON as hook_failures , and never fails the operation.

What a hook receives. Environment variables — unset (never empty) when the fact is not known at that moment:

Variable Meaning
THURBOX_HOOK_EVENT the event name, e.g. session.post_create
THURBOX_SESSION the thurbox session id (at pre_create: the id it will have if creation succeeds)
THURBOX_SESSION_ID the agent's own conversation id
THURBOX_SESSION_NAME, THURBOX_AGENT the session name and its agent
THURBOX_REPO the primary repository path
THURBOX_CWD the directory the agent runs in (the worktree, or the symlink workspace of a multi-repo session); unset at pre_create
THURBOX_BRANCH / THURBOX_BASE_BRANCH the worktree branch and what it was created from (base: create events only)
THURBOX_HOST the remote host name; unset for a local session
THURBOX_PARENT_SESSION the parent session id (a fork, or --parent)
THURBOX_TASK the originating task id, for a task-spawned session
THURBOX_CONFIG_DIR / THURBOX_DATA_DIR so a thurbox-cli run inside the hook hits the database of the thurbox that fired it
THURBOX_SOCKET the multiplexer socket that thurbox's sessions live on, so the same thurbox-cli reaches the same server rather than deriving one

The same facts — plus worktrees (repo_path, worktree_path, branch), additional_dirs , force (delete) and force_deleted (restore) — arrive as one JSON object on stdin (jq -r .cwd).

Where and how it runs. In the primary repository when that is a directory on this machine, otherwise in thurbox's own working directory — the repository is the one path that exists at every event (at pre_create the worktree is not made; at post_delete it is gone). With no terminal : stdin is the JSON, stdout/stderr are captured and only their tail (500 chars) is reported, so a hook can neither draw on nor read from the TUI's screen. A hook for a remote (SSH/WSL) session still runs locally ; THURBOX_HOST names the host and the paths are the host's — ssh "$THURBOX_HOST" … from the hook is your call.

thurbox-cli config validate strict-parses the file; config show lists the hooks in force.

settings.toml #

Scalar tuning knobs plus the [features] switches, seeded fully commented-out (defaults apply when absent). Only knobs a user plausibly wants are exposed; internals stay hardcoded. The file is live-reloaded via mtime poll: the UI-panel feature flags apply immediately, while the [notifications] knobs, the automations/mouse/notifications/version_check/auto_update feature flags, and the write-once scalars take effect on the next launch.

Settings panel. You don't have to hand-edit the file: the TUI has a Settings modal opened with Ctrl+, (or F6 ) that views and edits all of settings.toml — the [features] toggles, [notifications] knobs, and scalars. It edits a working copy and applies only on Ctrl+S (Esc discards); the panel writes back through the same file, preserving its documentation comments.

Key Default Purpose
scrollback_lines 1000 terminal scrollback kept per session
two_panel_min_cols 80 width below which only the terminal renders
three_panel_min_cols 120 width unlocking the optional third column
audit_retention_days 90 audit + session-event history kept (pruned on startup)

A complete settings.toml showing every knob at its default — copy this and uncomment what you want to change (the UI-panel feature flags apply live; the [notifications] knobs, the automations/mouse/notifications/version_check/auto_update flags, and the write-once scalars take effect on the next launch):

settings.toml
config_version = 1

# Scalar tuning knobs (top level)
scrollback_lines      = 1000   # terminal scrollback kept per session
two_panel_min_cols    = 80     # width below which only the terminal renders
three_panel_min_cols  = 120    # width unlocking the optional third column
audit_retention_days  = 90     # audit + session-event history kept (pruned on startup)

[features]
tasks         = true
automations   = true
file_viewer   = true
global_search = true
info_panel    = true
shell_pane    = true
code_review   = true           # accepted, gates nothing (the pane was removed)
mouse         = true
notifications = true
soft_delete   = true           # Ctrl+D soft-deletes (Ctrl+Z undo); false = hard-delete
version_check = false          # opt-in: makes a network call
auto_update   = false          # opt-in: downloads + replaces binaries

[notifications]
backend             = "auto"   # auto / dbus / windows / off
also_on_waiting     = false    # also fire on Busy → Waiting (no bell)
suppress_for_active = true     # skip the session you're currently viewing
sound               = true     # play the OS default notification sound
min_interval_secs   = 5        # per-session floor between notifications

[features] — whole-feature switches

Switch off behaviour that reaches outside the interface. All default to true — as of 1.0 that includes version_check and auto_update, which both reach the network (they were opt-in before 1.0). shell_pane, perf_hud and soft_delete apply live on save; automations, mouse, notifications, version_check and auto_update, and the write-once scalars, wait for the next launch. Data is never touched, so re-enabling a flag is lossless.

A pane is not switched off here. Panes are files, so turning one off is space on its row in the Interface tab (Ctrl+, then ]), recorded in ui.json. Five keys are still accepted and ignoredtasks, file_viewer, info_panel, code_review and global_search — because they gated panes the binary no longer draws. They are parsed rather than rejected so an existing settings.toml keeps loading, but setting one has no effect in either direction. Same for three_panel_min_cols.

Key Default Controls
tasks true Accepted, gates nothing — the tasks pane was removed and nothing reads this. thurbox-cli task works regardless
automations true heartbeat arming and headless firing. There is no pane, and no in-TUI scheduler — the tmux keeper runs due automations whether or not thurbox is open
file_viewer true Accepted, gates nothing — the file viewer was removed
global_search true Accepted, gates nothing — the search strip is a plugin, turned off from the Interface tab
info_panel true Accepted, gates nothing — the info panel was removed
shell_pane true per-session shell toggle (Ctrl+T)
code_review true Accepted, gates nothing — the review view was removed
mouse true mouse capture: clicks, wheel, drag-select, hover, scrollbars
notifications true OS desktop notifications when a session needs attention
soft_delete true TUI Ctrl+D soft-deletes (with a Ctrl+Z undo window); false makes it a hard delete behind a confirmation modal. Never affects thurbox-cli session delete (soft unless --force).
version_check false GitHub update check: TUI header “update available” badge + thurbox-cli version --check
auto_update false silent self-update: download + verify + replace the binaries on startup + thurbox-cli update

automations = false is the one flag with teeth beyond the UI: it also stops the TUI from firing due schedules and arming the tmux heartbeat at startup (explicit thurbox-cli automation commands still work). mouse = false skips terminal mouse capture entirely, so the terminal keeps its native mouse behavior. notifications = false keeps the background dispatcher thread from ever starting (zero overhead) and silently no-ops every transition; the session status display itself is unaffected.

version_check = true enables the update check. On launch the TUI reads a cached result (~/.local/share/thurbox/version-check.json) and, if it is older than 24 h, fires a single best-effort background fetch of GitHub's latest release (via curl/wget); a newer release shows a ↑ vX.Y.Z available badge next to the version in the header. The fetch never blocks startup and failures are silent. Dev builds (0.0.0-dev) never show the badge. The same flag enables thurbox-cli version --check (thurbox-cli version with no flag always prints the current version).

auto_update = true goes a step further than version_check: instead of just showing a badge, the TUI silently updates itself on startup. After the same 24 h cache check, if a newer release exists it downloads that release's tarball + checksums from GitHub Releases (curl/wget), verifies the SHA256 (sha256sum/shasum), extracts it (tar), and atomically replaces the installed thurbox/thurbox-cli binaries in place — mirroring scripts/install.sh. The download is verified before any installed file is touched, so a failed download leaves the current binaries untouched; the step runs before the TUI takes the terminal and is best-effort. The replaced binary takes effect on the next launch, so the TUI shows an “Updated to vX.Y.Z — restart to apply” status line. thurbox-cli update performs the same update on demand (--force bypasses the up-to-date, dev-build and major-version guards); dev builds (0.0.0-dev) never auto-update. version_check and auto_update are independent — enable either or both.

Auto-update never crosses a major version. A new major is reported — by the badge and by thurbox-cli version --check — and never installed, because a silent crossing could hand you a different program under the same binary name. thurbox-cli update --force is the deliberate way across.

[notifications] — OS notification settings

Surfaces an OS notification when a session crosses into a state that needs the user's attention (the agent rang the terminal bell or emitted an OSC 9 / OSC 777 message — usually because it's waiting on an answer or has finished a task). Linux dispatches via dbus and supports click-to-focus: clicking the banner writes a focus request that the running TUI reads on its next tick and switches to that session. macOS shows the banner but ignores clicks (the modern UNUserNotificationCenter API requires a signed app bundle, which thurbox is not). The notification body is the agent's last OSC message when present, otherwise Waiting for input. Only fires while the TUI is open — the agent terminal parser is what sees the bell, and it doesn't run when thurbox isn't. Gated by the notifications feature flag above.

Key Default Purpose
backend auto delivery backend (auto / dbus / windows / off): auto picks dbus on a Linux desktop, the WSL Windows-toast fallback, or the macOS native banner; off drops every notification (a soft switch distinct from the feature flag)
also_on_waiting false also fire on Busy → Waiting (no explicit bell from the agent)
suppress_for_active true skip the notification for the session you're currently viewing
sound true play the OS default notification sound
min_interval_secs 5 per-session floor between two notifications (dedup)

The default-on Attention trigger is the right knob for any agent that respects the terminal bell / OSC 9 / OSC 777 conventions (Claude Code out of the box, for example). For agents that only go quiet without ringing a bell, set also_on_waiting = true — note this fires once each time the agent goes idle after activity, so it can be chatty.

themes.toml #

User-defined themes, offered in the Ctrl+Y picker alongside the thirty-six built-in presets and persisted by name like any preset. Each [[themes]] entry starts from a built-in base and overrides only the colours it names:

themes.toml
[[themes]]
name = "my-mocha"            # stable id; must not shadow a built-in
display_name = "My Mocha"    # picker label (default: name)
base = "catppuccin-mocha"    # starting palette (default: default)
accent = "#fab387"
app_bg = "reset"             # keep the terminal's native background

Colours accept anything ratatui parses: #rrggbb , ANSI names ( red , lightcyan ), indexed ( 14 ), or reset . Bad colours and built-in name collisions degrade to startup warnings.

Rebound chords (ui.json) #

Rebindings live in ~/.config/thurbox/ui.json, beside the panes you have turned off and the files you trust — all three are decisions you made about the interface, which is why they share a file. Its bindings object maps an action id to the one chord string that action answers to. Action ids are dotted and lowercase — the kernel's own are help.open , settings.open , themes.open and palette.open , and panes declare theirs in the same style ( sessions.delete , sessions.restart , …):

ui.json
{ "bindings": { "themes.open": "f2", "sessions.delete": "f3" } }
  • The editing path is the F1 panel : select an action, press the chord. It takes effect on the next keystroke and persists itself.
  • Chord syntax: [ctrl+][alt+][shift+][cmd+]<key> where <key> is a letter, f1f12 , or a named key ( enter , esc , tab , arrows, home , end , pageup , pagedown , backspace , delete , insert ). Case-insensitive. cmd (aliases super , command , win ) is the macOS Command key, delivered only by kitty-keyboard-protocol terminals.
  • Binding a chord another action already claims in the same context takes it: your override is the one that fires. An entry naming an action no plugin declares is dropped silently — a typo just does nothing, which is why the F1 editor, which refuses an unknown action outright, is the safer path.
  • Five chords are reserved and cannot be rebound at all — ctrl+q (quit), f10 (reload), ctrl+h / ctrl+l (focus) and f12 (perf HUD). They are the way out of a pane that consumes every key, so quit is not a declared action and has no id to bind.

See the Keybindings page for the full default chord table.

Extensions #

Each opt-in extension is described by a single extension.toml manifest. thurbox-cli extension install writes the home-resolved copy to ~/.config/thurbox/extensions/<name>.toml (thurbox never seeds this dir). The manifest has an install spec and a runtime spec; see the Extensions page for the full manifest format, lifecycle commands, and versioning.

bash
thurbox-cli extension install ./my-ext     # fetch + lay files + agents + activate
thurbox-cli extension list                 # installed + active/healthy + version/stale
thurbox-cli extension update --all         # re-fetch every installed extension
thurbox-cli extension activate <name>      # (re)create resources + mark active
thurbox-cli extension deactivate <name>    # tear down + stop self-heal
thurbox-cli extension status [<name>]      # per-resource presence + version/stale

SQLite-backed settings #

Live in the metadata table and apply immediately (no restart):

Key Set via Purpose
active_theme Ctrl+Y / F4 picker TUI palette (thirty-six built-ins)
editor_command thurbox-cli editor set "<cmd>" what Ctrl+O runs
active_extensions extension activate/deactivate JSON array of active extensions to self-heal

These are in the DB rather than a file because they are written concurrently by multiple thurbox processes (TUI, CLI, MCP) and picked up live via PRAGMA data_version polling.

Environment variables #

Variable Used for
XDG_CONFIG_HOME, XDG_DATA_HOME config/data roots
VISUAL, then EDITOR Ctrl+O editor when editor_command is unset
SHELL the Ctrl+T companion shell pane (fallback /bin/sh)
RUST_LOG log filter for thurbox.log
THURBOX_CONFIG_DIR / THURBOX_DATA_DIR relocate this instance's config / database (see below)
THURBOX_SOCKET name the multiplexer socket outright, overriding both

Relocating an instance. THURBOX_CONFIG_DIR and THURBOX_DATA_DIR move thurbox's config and its database, and an instance whose data dir has moved also gets a multiplexer socket of its own , so it cannot create windows on the server holding your everyday sessions. A default instance keeps the socket it has always used — nothing changes for a thurbox you have not relocated. Ask a build which server it is on rather than assuming: thurbox-cli version --json reports it as tmux_socket . The full rules, and what happens to an instance relocated before this existed, are in docs/CONFIG.md → Relocating an instance.

Every lifecycle hook additionally receives the THURBOX_* facts of the session it concerns (event, id, name, agent, repository, working directory, branch, host, parent) and the config/data-dir overrides; the full list is in the hooks.toml section above.

Editor resolution order: DB editor_command$VISUAL$EDITOR → error toast.

Versioning #

The SQLite schema migrates automatically ( schema_version in metadata ). The TOML files carry a config_version = 1 marker so a future format change can migrate them too; current files are version 1 and the field is optional.