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

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:

TaskWhat it does
just buildbuild the dev binaries (thurbox + thurbox-cli)
just testcargo nextest run --all
just test-scriptsthe bats suites: scripts/install.bats + the pull-request-title checker (needs bats)
just lintfmt-check + clippy + cargo-deny + rumdl + shellcheck + selene, stylua and lua-language-server
just fmtformat Rust + website
just archarchitecture-rule + rustdoc checks
just hooks-installprek install
just smokeblack-box TUI test: the real binary on a pty (tests/tui_e2e.rs)
just benchwhat a frame costs, piece by piece (benches/frame_cost.rs)
just perfwhat 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-devdev_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; only thurbox-config + thurbox-data (+ a private TMUX_TMPDIR) are redirected. Use this to dev with your real, logged-in agents without polluting your real thurbox state.
  • full (--isolate-home) — also overrides HOME + XDG_*, so the env is hermetic and agents boot with no credentials. This is what scripts/demo/ record.sh uses (via tbx_sandbox_init_full).

Profile lifetimes:

  • Persistent profiles live under target/dev-sandbox/<profile>/ (gitignored; cargo clean or --clean removes them). Their tmux socket dir is kept short under $XDG_RUNTIME_DIR (AF_UNIX socket paths are length-limited, and the repo's target/ path is often too long). Sessions survive across runs.
  • Fresh (--fresh) is a mktemp dir 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'