Development
September 12, 2026 · View on GitHub
How to set up a reproducible thurbox dev environment and run the app in an isolated sandbox.
1. Toolchain — the dev environment
Recommended: Nix flake
The flake.nix provides the tools CI uses — the Rust toolchain (read from
rust-toolchain.toml), tmux, shellcheck, bats, Node, cargo-nextest,
cargo-deny, cocogitto, just, and the demo stack (vhs/ffmpeg/ttyd).
It does not pin versions: no flake.lock is committed and
rust-toolchain.toml says stable, so a fresh checkout resolves whatever is
current. No CI workflow uses Nix — this is a local convenience, not the thing
CI runs.
# one-time, if not done already: enable flakes
# mkdir -p ~/.config/nix && echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
nix flake lock # one-time: generate/commit flake.lock (pins inputs)
nix develop # enter the pinned shell
# ...or, with direnv installed, once:
direnv allow # auto-enters the shell on `cd` (see .envrc)
A couple of tools aren't packaged in nixpkgs yet (prek, rumdl, nightly
cargo-pup); the shell prints a hint to install them via
scripts/install-dev-tools.sh.
Fallback: no Nix
scripts/install-dev-tools.sh # cargo-binstall/cargo install the dev tools
prek install # install the git hooks
You'll also need, from your package manager: tmux >= 3.2, shellcheck,
bats, Node + npm (website linters), git, and the three Lua gates just lint
runs — selene, stylua and lua-language-server. Run npm ci once, or
just fmt's website half exits 127 on a fresh checkout.
2. Everyday tasks — just
just (in the dev shell) is the task entrypoint — run just for the list:
| Task | What it does |
|---|---|
just build | build the dev binaries (thurbox + thurbox-cli) |
just test | cargo nextest run --all |
just test-scripts | the bats suites: scripts/install.bats + the pull-request-title checker (needs bats) |
just lint | fmt-check + clippy + cargo-deny + rumdl + shellcheck + selene, stylua and lua-language-server |
just fmt | format Rust + website |
just arch | architecture-rule + rustdoc checks |
just hooks-install | prek install |
just smoke | black-box TUI test: the real binary on a pty (tests/tui_e2e.rs) |
just bench | what a frame costs, piece by piece (benches/frame_cost.rs) |
just perf | what the whole binary costs under load (scripts/dev/perf-run.sh) |
just sandbox* | dev runtime sandbox (below) |
just bench and just perf are the two measuring instruments, and they answer
different questions: the bench times the pieces of a frame against the real
ui/, while just perf runs the whole binary against real tmux panes and
reports CPU. Neither is in CI — wall-clock timing stays out of the gate
(ADR-P5) — and a claim from either is a paired before/after at a stated
terminal size and session count, never a single absolute number. Both are
explained in docs/PERFORMANCE.md.
3. Runtime sandbox — run thurbox isolated
The sandbox runs the dev build (0.0.0-dev → dev_build cfg, which uses a
thurbox-dev tmux socket) with thurbox's own config/data redirected into the
sandbox (via THURBOX_CONFIG_DIR / THURBOX_DATA_DIR), so it never touches your
real ~/.config/thurbox or sessions. It keeps your real HOME, so your
authenticated agent CLIs (claude/codex/antigravity/…) work normally — and it puts
the dev target/debug first on PATH, so an agent's status hook calls this
thurbox-cli and writes to the sandbox DB the TUI reads. The tmux socket is
scoped twice over: a private TMUX_TMPDIR, and THURBOX_SOCKET naming the
server outright — a relocated THURBOX_DATA_DIR derives a socket of its own
(docs/CONFIG.md → Relocating an instance), and teardown kills the socket by
name.
scripts/dev/sandbox.sh # persistent "default" profile, launch the TUI
scripts/dev/sandbox.sh --fresh # throwaway env, wiped on exit
scripts/dev/sandbox.sh --profile foo # a named persistent profile
scripts/dev/sandbox.sh --isolate-home # full hermetic isolation (fresh HOME; agents have NO creds)
scripts/dev/sandbox.sh --shell # a shell with the sandbox env (run thurbox-cli by hand)
scripts/dev/sandbox.sh -- session list # run a thurbox-cli command in the sandbox
scripts/dev/sandbox.sh --clean [name] # kill + wipe a persistent profile
Or via just: just sandbox, just sandbox-fresh,
just sandbox-shell, just sandbox-clean [profile].
The sandbox points THURBOX_CONFIG_DIR at its own root, so the interface
materialises at <sandbox>/thurbox-config/ui/ along with agents, settings and the
database. --fresh is therefore a clean first-run interface every time — which is
how the plugin lifecycle (delivery, removal, restore) is exercised without touching
your real one.
The TUI is launched from the sandbox root rather than the repo. Where you stand no longer decides which interface loads, so this is belt-and-braces.
To run the dev TUI against this checkout's ui/ instead of a copy, ask for it:
just tui-ui (which sets THURBOX_UI_DIR).
Isolation flavors:
- thurbox-only (default) — real
HOME/agents; onlythurbox-config+thurbox-data(+ a privateTMUX_TMPDIR) are redirected. Use this to dev with your real, logged-in agents without polluting your real thurbox state. - full (
--isolate-home) — also overridesHOME+XDG_*, so the env is hermetic and agents boot with no credentials. This is whatscripts/demo/ record.shuses (viatbx_sandbox_init_full).
Profile lifetimes:
- Persistent profiles live under
target/dev-sandbox/<profile>/(gitignored;cargo cleanor--cleanremoves them). Their tmux socket dir is kept short under$XDG_RUNTIME_DIR(AF_UNIX socket paths are length-limited, and the repo'starget/path is often too long). Sessions survive across runs. - Fresh (
--fresh) is amktempdir wiped on exit — same isolation the demo recorder uses.
The isolation logic is one helper, scripts/dev/lib/sandbox-env.sh, sourced by
scripts/dev/sandbox.sh and scripts/demo/record.sh (one source of truth).
tests/tui_e2e.rs isolates the same way in Rust — private profile dirs, a short
private TMUX_TMPDIR — so it never touches a real profile either.
Example: watch a session's status hook end-to-end
scripts/dev/sandbox.sh --shell
# inside the sandbox shell (thurbox/thurbox-cli target the sandbox):
thurbox-cli session create --name demo --repo-path "$PWD" --agent claude
thurbox-cli session signal --state blocked --session <id> # what an agent hook does
thurbox-cli session list --json | jq '.[].name'