← Back to portfolio
B
Case Study · Archived experiment5 min read

One window for
a fleet of Claude sessions.

A desktop app that drives many Claude Code sessions at once. See every session in a sidebar, type into any one, watch it stream, and approve its tool calls inline. Built on the Agent SDK, in three isolated process layers. An experiment I took deep, then set aside: it was time-intensive to maintain, and isn’t something I actively develop now.

N
Sessions at once
1
Window
3
Process layers
0
Terminals
Scroll to read
00The Problem

Ten terminals, no cockpit.

Driving several Claude Code sessions at once means juggling ten terminal windows, one per project or task. There is no single surface to see what each session is doing, type into a specific one, or approve its tool calls without alt-tabbing between them.

I wanted a cockpit: one window that drives every session, shows each one’s live state, and lets me approve a tool call in the session that asked for it, without losing the others.

01The Shape

One window, three isolated layers.

Basalt is one desktop window driving N concurrent Claude Code sessions, split into three processes that each do one job. A Tauri shell in Rust owns the native window and supervises the rest. A React frontend renders the fleet. A Node sidecar holds the engine.

The Rust shell carries no business logic. It opens the window, spawns the sidecar with a random port and auth token, restarts it if it crashes, and kills it on exit. The frontend talks to the sidecar over a localhost-only WebSocket gated by that token, so no other process on the machine can connect. Tauri over Electron keeps the whole app around 10 to 15 MB instead of 100-plus.

Tech Stack
Tauri 2 (Rust)React + ViteNode sidecarlocalhost WebSocket
How it works
Tauri hands the frontend a port and token at startup; the frontend opens a 127.0.0.1 WebSocket with the token. Lifecycle in Rust, brains in the sidecar, pixels in the frontend.
Why it matters
Three processes with clean seams. The sidecar can die and restart without taking the window down, and the frontend resyncs from it as the source of truth.
In practice
About 10 to 15 MB, versus 100-plus for an Electron equivalent.
02The Engine

Built on the Agent SDK, not a terminal.

Each session is a real Claude Agent SDK query(), not a wrapper around the terminal UI. I chose the SDK over PTY-scraping the TUI (brittle, no structured results) and over headless claude -p (no live tool-approval callback).

The SDK gives what a controller actually needs: a canUseTool callback to intercept every tool call, partial-message streaming for live output, per-session interrupt, model and permission-mode control, and many concurrent sessions in one process. A SessionManager owns the registry; each Session wraps one query() and holds its own transcript. One session erroring is marked errored and never touches its siblings.

Tech Stack
Claude Agent SDKcanUseToolincludePartialMessagesconcurrent query()
How it works
Sessions reuse your existing Claude Code login (the SDK shares the CLI’s auth), so there is no new credential to handle.
Why it matters
Structured events beat scraped text. Basalt renders what the model actually did, not characters it printed.
In practice
interrupt(), setModel(), setPermissionMode() per session, all in one Node process.
03Driving The Fleet

A transcript, not a terminal.

Because the SDK emits structured events, the UI is a clean chat-style transcript instead of a terminal emulator. Text deltas coalesce into blocks; each tool call is a card; a card awaiting permission becomes an approval card with approve, deny, or edit-the-input.

A left rail lists every session as a live status row: a colored dot, the label, and a one-line activity such as editing or needs approval. The main pane is the selected session’s transcript and composer. The whole thing wears an Ember identity, a near-black basalt palette with a lava-amber accent, where amber always means a tool is waiting on you.

Tech Stack
ReactZustandStructured event streamNo xterm.js
How it works
A single Zustand store is the only thing touching the socket; WebSocket events reduce into it, and thin actions send commands back down.
Why it matters
You drive any session without losing the others. Approve the call in the session that asked, while the rest keep streaming.
In practice
No xterm.js. Structured events in, a chat log out.
04Coordination

Many sessions, one repo, no collisions.

The harder problem is running several focused sessions in the same repository at once without them overwriting each other. Basalt keeps one working tree and prevents collisions by locking files at edit time.

The lock gate is a PreToolUse hook, deliberately not the canUseTool callback. canUseTool is skipped in the permissive permission modes you actually use for fleet work, while a PreToolUse hook fires in every mode and can block. When a session tries to write a file another session holds, Basalt denies the call with a reason, and the blocked session’s Claude reroutes itself. Zero trust in the models cooperating. A live board, a single-writer .basalt/board.md, shows who is touching what.

Tech Stack
PreToolUse hookLockRegistryPer-file locks.basalt/board.md
How it works
A LockRegistry maps each path to its owning session; toolTargets extracts the written path from Edit, Write, MultiEdit and NotebookEdit. Conflict means deny with a reason; free means acquire and allow.
Why it matters
Locks beat git-worktree isolation here: one tree, no branch juggling, and collisions are impossible instead of merged later.
In practice
Basalt is the sole author of the board file, so there is no race on the coordination file itself.
05Built To Run Daily

Built, and running every day.

All of it shipped: the sidecar engine, the frontend, the Tauri shell, file-lock coordination, and lazy session resume that survives closing the app. It installs as a real Basalt.app and rebuilds with a single studio basalt update.

The sharp edge was packaging. A double-clicked .app gets a minimal PATH, so the user’s node isn’t on it; Basalt resolves the real binary through the login shell at startup and spawns it directly. A fake-engine mode swaps the SDK for a scripted generator, so the entire UI can be built, demoed, and screenshotted without spending a single token. For now it is daily studio tooling, proving it holds up before it ships in the open.

Tech Stack
pnpm tauri buildSidecar as Tauri resourceLogin-shell PATHFake-engine mode
How it works
pnpm tauri build bundles the sidecar as a Tauri resource (a real, deployed node_modules); Rust resolves it from the resource dir when bundled.
Why it matters
It earns its place by being the tool I actually drive the studio with, every day.
In practice
Lazy resume: prior sessions show as dim rows and reattach only when you click them.
The Bigger Picture

The third
pillar.

Three experiments, one studio. The harness operates it, Lattice remembers it, and Basalt explored driving a fleet of agents at once: built deep, then set aside as too time-intensive to keep up. Each one exists because the tool I needed didn’t yet.