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:
thurbox-cli config validate # strict parse of every file; exit 1 on problems
thurbox-cli config show # effective config + where each value came fromWhich 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.
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]]
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):
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 ignored — tasks,
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]]
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
, …):
{ "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,f1–f12, or a named key (enter,esc,tab, arrows,home,end,pageup,pagedown,backspace,delete,insert). Case-insensitive.cmd(aliasessuper,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) andf12(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.
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/staleSQLite-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.