Two clients sharing a multipass sandbox — summon, drive the shell, real per-user sudo.
Fork of cmd-chat — a privacy- and security-oriented chatroom, extended with file sharing and shared terminal sessions.
Encrypted chat that runs in your terminal. You host the server, you control the room. Close the window — everything's gone. Messages and files are encrypted client-side before the server ever sees them. Built for people who'd rather not trust a corporation with their conversations: pairing, teaching, demos, hacking.
- End-to-end encrypted — Fernet (AES-128-CBC + HMAC), encrypted client-side before anything leaves your machine
- SRP authentication — the password is never sent over the network (zero-knowledge proof)
- Zero-knowledge server — relays only ciphertext; cannot read messages, files, or terminal output
- RAM only — nothing persisted on the server; close it and history is gone
- Shared sandbox — summon a disposable
local/docker/podman/multipassbox the whole room can watch and drive. Docker defaults to Parrot OS Security (parrotsec/core) and Podman to Kali (kalilinux/kali-rolling) — pentest distros out of the box; Podman is rootless & daemonless, so no sudo to launch - Snapshot save/load — freeze a sandbox to a named snapshot and restore it later (
/sbx save·/sbx load·/sbx snaps) - Local VirtualBox VMs —
/sbx vmsdetects VirtualBox and lists your VMs;/sbx gui <vm>opens a desktop VM locally for the room to gather around — per-user consent gate, with automatic resolution of VT-x conflicts (Docker Desktop / multipass) - Physical device bridge — bring a real device into the room as a first-class, driveable member with
/sbx pager(WiFi Pineapple Pager over SSH;/sbx device <alias>for others). It joins like a headless member and reuses the same_sbx/ACL control plane, so drive and the shared-terminal view work exactly as they do for a container - Browser guests over Tor — a phone with only a browser (no hack-house install) can watch, chat, and request drive: the in-tree web relay + Tor publisher front the room behind a second throwaway onion. Fully merged into
main(see Browser access) - Real permissions — the host grants/revokes drive (keyboard) and sudo (VM superuser) per user; stacking roster badges show exactly who holds what, both in the clergy panel and inline on every chat message; the host can
/kicka member (which also rotates the room password) - Local-first AI agent —
/ai startsummons an in-room AI that runs against your own Ollama (no API key, nothing leaves your machine); replies stream token-by-token with in-RAM semantic recall of the conversation for context; model-agnostic, addressed-only, end-to-end encrypted like every other client - AI that acts in the sandbox — grant an agent drive and address it with
/ai <name> !<task>; it works the shared box through a bounded, host-side tool-calling loop (run shell, write/read files, inspecting each result before the next step) and you watch its commands land live in the shared terminal. Ungranted, it stays advisory-only (tells you the commands, runs nothing); destructive commands are gated behind an explicit/ai <name> confirm - Encrypted file transfer —
/send→/acceptwith SHA-256 verification - Tor onion hosting (optional) — expose a room via a per-session, throwaway Tor v3 onion address instead of any inbound port (
--tor); the key is never written to disk and the address is gone the moment the server stops. Pair it withweb-relayfor a second onion + browser access (Orbot/Tor Browser) so a guest with no hack-house install — just a phone — can watch and chat too. See Tor onion hosting. - TLS — self-signed by default, or bring your own cert;
--no-tlsfor local/Tailscale use - Themes — seven switchable "vestments" (
cryptdefault ·church·neon·blush·matrix·wraith·goldcrypt), plus a live randomizer
| Path | What |
|---|---|
hh/ |
The Rust ratatui client (the flagship) |
cmd_chat/, cmd_chat.py |
The Python (Sanic) server + legacy Python client |
cmd_chat/agent/ |
The model-agnostic AI agent bridge (joins a room as an encrypted client) |
cmd_chat/web/, web-relay/ |
Browser-guest support: the room→browser publisher (cmd_chat.web) + the loopback web relay it fronts (see Browser access) |
cmd_chat/device/ |
Physical-device bridge — joins a real device (/sbx pager) as a driveable room member (see The physical device bridge) |
models.toml |
Named provider profiles for /ai start <profile> (see docs/providers.md) |
docs/providers.md |
Connect any model — profiles, flags, discovery, bring-your-own-provider |
hh/scripts/ |
Helper scripts — setup, hosting, sandbox provisioning, tests (see Scripts; each takes -h/--help) |
hh/direnv-autostart/ |
cd into a directory to auto-launch a session (direnv) |
cmd_chat/tor/ |
Ephemeral Tor v3 onion service sidecar (--tor on serve) — see Tor onion hosting |
scripts/tor-hardened-launch.sh |
Sandboxed Tor launcher (bwrap by default) for --tor hosting |
scripts/tor-onion-connect.sh |
Guest-side helper: finds a checkout + client regardless of cwd, wraps torsocks |
docs/spec-tor-p2p-relay.md |
Full design + OPSEC writeup for Tor onion hosting |
One line — copy, paste, enter:
git clone https://github.com/leetcrypt/hack-house.git && cd hack-house && bash hh/scripts/bootstrap.shThat clones the repo, checks prerequisites (Python 3 + Rust/Cargo), creates the Python venv, installs the server, and builds the Rust client. No prompts. When it finishes, start a local room and take your own seat:
hh/scripts/hh-go.sh up # host a room + open your client (all in tmux)That is the whole baseline — an encrypted room with a shared sandbox. Everything below is optional.
Prefer to choose as you go? Run the interactive installer instead of the
one-liner — it walks you through the build type, the AI layer (none / local
model / bring-your-own), and putting hh-go on your PATH:
git clone https://github.com/leetcrypt/hack-house.git && cd hack-house && bash hh/scripts/install.shhh-go — one command for everything. After setup (the installer offers to
symlink it onto your PATH), hh-go routes the common tasks:
hh-go up # host a local room + your own seat
hh-go host tailnet|lan|tor # host the server only (prints the join command)
hh-go join <host> <port> # connect to someone else's room
hh-go ai [model|profile] # summon an AI model you own into your room
hh-go device flipper # bridge a physical device into the room
hh-go share # start browser sharing (then /share in the TUI)
hh-go status | down # what's running / tear it downThe in-room /ai agent runs a local model via Ollama (a multi-GB download),
so it is opt-in — nothing above needs it:
hh/scripts/bootstrap.sh --ai # baseline + Ollama + a default model
hh/scripts/bootstrap-ai.sh # or add it later (baseline already done)
HH_AI_MODEL=llama3 hh/scripts/bootstrap-ai.sh # pull a different modelCloud or custom models (Anthropic, OpenAI, Groq, vLLM, or your own) need no
Ollama — see docs/providers.md.
hh/scripts/bootstrap.sh --release # build the client in release mode (faster)
hh/scripts/bootstrap.sh --check # report tooling only, change nothingThe fastest way to see it working: builds the client, boots a fresh --no-tls
server on 127.0.0.1:4173, and opens a pane per user.
cd hh
./scripts/lets-hack.sh # alice + bob, tiled in tmux
./scripts/lets-hack.sh neo trinity # custom users
./scripts/lets-hack.sh --theme neon # pick vestments
./scripts/lets-hack.sh --reuse # keep a live server (reconnect tests)
./scripts/lets-hack.sh --kill # tear it all downServer (Python):
pip install -r requirements.txt
python3 cmd_chat.py serve 0.0.0.0 3000 --password <room-password>Client (Rust):
cd hh
cargo build --release
./target/release/hack-house connect <server_ip> 3000 <yourname> \
--password <room-password> --insecure| Flag | Purpose |
|---|---|
--password |
Room password (required) |
--no-tls |
Connect without TLS (local / trusted tunnel) |
--insecure |
Skip TLS cert verification (self-signed certs) |
--theme <path> |
Load a vestments TOML (see hh/themes/) |
A convenience for daily use, independent of bootstrap.sh. Run the one-time
setup once:
cd hh/direnv-autostart
./setup.sh # installs direnv, hooks bash/zsh, `direnv allow`s this dirAfter that, simply cd-ing into the directory launches a single session for the
logged-in user with a freshly minted in-memory room password (reveal it
in-app with /pw, share it out-of-band to invite others). The password is
generated at launch and never written to disk — matching the project's RAM-only
model. If a session is already live, it just points you at it.
Type to chat. Slash commands and keys:
| Command / key | Action |
|---|---|
<text> ↵ |
Send an encrypted chat message |
/help · F1 |
Help overlay |
/pw |
Show this room's password (local only — never broadcast) |
/share |
Print a paste-ready invite block for this room (local only): the password plus every way in — the Tor onion (if hosted --tor) and each reachable tailscale / lan / public address, each with its connect command. Loopback-only rooms say so and how to make them reachable |
/theme [name] |
Switch vestments, or list them |
/send <user> <path> |
Offer a file (or directory) directly to one member |
/sendroom <path> |
Offer a file (or directory) to the whole room |
/accept · /reject |
Respond to a pending file offer |
/clear |
Wipe your chat scrollback (local only) |
/ai start [model|profile] [allow] |
Summon a local AI agent (default ollama/qwen2.5:3b; a bare name is a models.toml profile). allow auto-grants it sandbox drive at spawn. Owner or a granted driver only — the agent runs against that caller's own Ollama and joins the room, so summoning is a privileged action (/ai list//ai models stay open to everyone) |
/ai stop |
Dismiss the agent you summoned |
/ai <question> |
Ask the agent (/ai <name> <question> if several present) |
/ai <name> !<task> |
Have a granted agent act in the shared sandbox (advisory-only if it has no drive) |
/ai <name> confirm |
Approve a gated (destructive) command the agent proposed |
/ai list |
List the agents present (or hint to /ai start if none) |
/ai models |
Models the active agent can serve — or, with no agent, your local Ollama tags |
/sbx <local|docker|podman|multipass> [image] [install] |
Summon the shared sandbox — the backend leads (/sbx launch … still works). Docker → Parrot OS (parrotsec/core), Podman → Kali (kalilinux/kali-rolling, rootless, no sudo). install fetches a missing backend; docker --start boots a stopped daemon |
/sbx pager · /sbx device <alias> |
Bring a physical device into the room as a driveable member (WiFi Pineapple Pager over SSH; pager is sugar for the pager alias). See The physical device bridge |
/sbx stop |
Tear down the sandbox you host |
/sbx save [label] · /sbx load <label> · /sbx snaps |
Snapshot the sandbox, restore one, or list snapshots |
/sbx vms |
Detect VirtualBox and list local VMs |
/sbx vbox [new [name]] |
Open the local VirtualBox VM picker, or build a fresh VM via cloud-init |
/sbx gui <vm> [--install] |
Open a local VirtualBox desktop VM for the room (consent-gated) |
/drive · F2 |
Take the shared shell (Esc releases) |
/grant <user> · /revoke <user> |
Owner: delegate/withdraw drive |
/sudo <user> · /unsudo <user> |
Owner: delegate/withdraw VM superuser |
/kick <user> |
Host only: force-disconnect a member and rotate the room password so the shared secret can't rejoin them |
Ctrl+C · Ctrl+Q |
Quit gracefully |
Ctrl+C (while driving) |
Interrupt the running command |
Ctrl+R |
Reconnect after a drop |
↑/↓ · PgUp/PgDn · mouse wheel |
Scroll chat / sandbox scrollback |
F4 · F5 · click |
Layout: fullscreen terminal · select a pane to resize (then arrows · Esc) — see Window layout |
/layout save | load | list | rm | reset |
Save / recall named pane arrangements |
Anyone in the room can summon a disposable Linux box with /sbx <backend>. The
person who summons it is the owner/host: their client runs the real PTY
locally and relays its output to everyone else as encrypted frames, so the
server only ever sees ciphertext (same trust model as chat).
| Backend | Isolation | Notes |
|---|---|---|
local |
none | a bash shell on the host — fast, for dev/testing only |
docker |
container | Parrot OS Security (parrotsec/core) by default — swap parrotsec/security per-launch for the full pentest set; /sbx docker --start boots the daemon (or run hh/scripts/ensure-docker.sh) |
podman |
container | Kali rolling (kalilinux/kali-rolling) by default — rootless & daemonless, no sudo to launch (add kali-linux-headless for the toolset); hh/scripts/ensure-podman.sh installs it |
multipass |
full VM | 24.04 by default; strongest isolation, ~30 s to boot, the choice for real use |
pager / device |
physical device | not a sandbox — bridges a real device (WiFi Pineapple Pager) into the room over SSH as a driveable member. See The physical device bridge |
The backend leads the command — /sbx podman, /sbx docker, /sbx multipass,
/sbx local (the older /sbx launch <backend> still works). Override the image
positionally, e.g. /sbx docker parrotsec/security or /sbx podman ubuntu:24.04.
Both container engines are Debian/apt-based, so the dev-toolchain bootstrap runs
unchanged. Tear it down with /sbx stop (purges the VM/container).
Snapshots. Freeze the current sandbox to a named checkpoint with /sbx save [label], list what you've stored with /sbx snaps, and restore one later with
/sbx load <label> — handy for resuming a half-built environment or replaying a
demo from a known-good state.
Sandbox egress (HH_SBX_EGRESS). Controls the summoned sandbox's outbound
network — orthogonal to how the room is reached (a Tor onion or tailnet front
door proxies inbound access; it never routes the container's egress). Set the
env var before summoning (the operator CLI also takes a per-launch sbx launch --egress <mode>):
| Mode | Outbound behaviour |
|---|---|
auto (default) |
Tor exit if Tor is available on this host, else local; and if the Tor gateway can't come up it falls back to local. A launch is never refused for egress reasons — no false-negative "won't connect". |
tor |
Force all outbound through a Tor exit (anonymized). |
local |
Block the LAN/host/tailnet pivot; allow internet (presents your real IP). |
scope |
local plus the hosts in $HH_SBX_SCOPE (comma-separated allowlist). |
open |
No restrictions (warn-only). |
none |
No egress at all (--network=none). |
guard |
Fail-closed leak-guard: refuse a networked launch unless the host's default route is a VPN/Tor tunnel. Opt-in — pick this when a dropped VPN must never leak the real IP. |
Example: export HH_SBX_EGRESS=guard before launching, or
… sbx launch --egress tor. local/scope/tor run the sandbox behind a
sidecar egress gateway (nftables + Tor) so the filtering lives outside the
sandbox's control.
Local VirtualBox VMs. Separate from the relayed sandbox, /sbx vms detects a
VirtualBox install and lists your VMs, and /sbx gui <vm> boots one as a full
desktop VM on your own machine (--install offers to install VirtualBox
first if it's missing). It's not owner-gated — the per-user confirmation gate
is the permission, so everyone opens their own copy. If a hardware hypervisor
(Docker Desktop, multipass) is holding VT-x, hack-house detects the conflict and
offers to stop it so the VM can boot.
A physical device can join a room as a member the same way the web publisher
bridges a browser: a headless client (cmd_chat/device/) joins, talks to the
device over its transport, and exposes it through the room's shared terminal —
reusing the exact _sbx/ACL control plane, so drive grants and the
watch-by-default view behave identically to a container. Members drive it with
the same /drive (F2) they use for any sandbox.
Launch it from the shell (or hh-go device <name>):
hh-go device flipper
# equivalently, by hand against any room/host:
.venv/bin/python -m cmd_chat.device <server_ip> <port> \
--device flipper --alias flipper --password <room-pw> --no-tlsSupported devices (adapters in cmd_chat/device/adapters/):
- Flipper Zero (
--device flipper) — over USB serial (/dev/ttyACM*). Plug it in, unlock it, and the bridge exposes the Flipper CLI (device info, GPIO, SubGHz, NFC/RFID, the filesystem under/ext) as room-driven commands. No network access to the device required. - Generic serial / SSH — the bridge speaks two transports,
serial_conn(USB) andssh_conn(network), so any device you can reach over a USB serial console or SSH can be fronted.
Bring your own device. Adapters are small: subclass the base adapter in
cmd_chat/device/adapters/, declare your device's verbs, and register it in
ADAPTERS (see flipper.py as the template). A device that already exposes an
SSH login needs no code — bridge it with --device ssh --alias <your-ssh-host>.
A WiFi Pineapple / Pager (Hak5) works via the same SSH-transport pattern (reachable over USB
172.16.x/ WiFi); its offensive-payload adapter is not bundled in the public build — write a thin adapter againstssh_connfor the verbs you need, followingflipper.py.
The shared terminal is watch-by-default: everyone sees the live output, but only granted drivers can type into it.
/drive(orF2) takes the keyboard;Escreleases it./driveexists so the whole flow works on mobile/SSH clients with no function keys.- While driving, your keystrokes go to the PTY;
Ctrl+Cinterrupts the running command (it does not quit the app). PgUp/PgDnand the mouse wheel scroll the terminal's scrollback even while driving;Endjumps back to live.
Permissions are enforced at two layers:
- App-level drive ACL — who is allowed to type into the shared shell.
The owner runs
/grant <user>//revoke <user>. - Real VM identities — on
multipass/docker, each member is provisioned an actual unix account, with the owner as superuser. Onmultipass,/sudo <user>//unsudo <user>toggle realsudorights inside the VM, so "may type" and "may run privileged commands" are independent and enforced by the OS itself.
The roster shows each member's status with stacking badges: host (the
theme's sigil, e.g. ✝), sudoer (⚡), driver (◆), and member (•). They're
additive — a host who summoned a sandbox and can drive reads ✝⚡◆ — and the
host badge appears the moment someone is first in the room, before any sandbox
exists. The same badge is rendered inline next to the author on every chat
message, so a message's authority is legible right in the transcript, not only
in the side panel. Because the badges read the exact ACL the sandbox enforces,
they can never advertise a power the room won't honour.
/send <user> <path> proposes a transfer to one member; /sendroom <path>
offers it to everyone. Recipients /accept or /reject. A whole directory
works too (it's packed into a .tar before sending). Files are chunked (64 KB),
encrypted with the room key, relayed as opaque ciphertext, and SHA-256
verified on arrival before landing in ./downloads/. Max size is 50 MB.
Let someone watch (and, if you grant it, drive) the shared terminal from a browser — no hack-house install on their side. Two steps:
hh-go share # starts the web relay + a publisher that joins your roomThen, inside the TUI, type /share — it prints the browser URL to copy to
your guest:
/share
…
[web] http://<relay>/r/<slug>#k=<key> ← the browser link
The link is a bearer credential — anyone with it can read the terminal and
chat, so hand it out out-of-band. The #… fragment is an end-to-end key that
never reaches the relay, which is why /share reads it from a local file the
publisher wrote and only shows it on the publisher's own machine (not across the
network). Browser guests appear in the roster as 🌐 and can be given drive with
/web allow <n>.
- Same machine / LAN / Tailnet: the above is all you need — hand out the printed link (bind the relay on your tailnet for remote guests).
- A phone with only a browser, anonymously: front the relay with Tor so the guest needs just Orbot or Tor Browser — see Browser access for mobile under Tor hosting.
Summon an AI participant with /ai start — it joins the room as a normal
encrypted client (same SRP + room key as everyone else) and answers when you
address it with /ai <question>. /ai stop dismisses it. Pick a model at summon
time with /ai start <model>, or from the shell with hh-go ai <model>.
You own what you summon (multi-tenant). Any member with sandbox drive can
host a model, and the member who summoned it owns that instance — not the
room host. As the owner you decide who may query your model, right from the
TUI (sole-form /ai <verb> targets the instance you host; /ai <name> <verb>
names one):
| Command | Effect |
|---|---|
/ai public · /ai private |
anyone may query (minus rejects) · allowlist only |
/ai allow <user> · /ai reject <user> |
add / deny a member on your instance |
/ai grant <user> · /ai revoke <user> |
delegate (or withdraw) management of your instance |
/ai ask-mode on|off |
hold each non-owner prompt for your approval |
/ai approve <n> · /ai deny <n> |
rule on a held prompt |
The roster marks instances 🤖 <name> ⟢<owner>, with 🔒 when private and ⧗
when prompts are held. Query control is yours; shared-sandbox drive stays the
room host's — an instance can only touch the shared shell after the host
/grants it. /ai list and /ai models are open to everyone for discovery.
Local-first, or bring your own. The default is a local model via Ollama —
private, no key, nothing leaves your host. To use any other model instead
(Anthropic, OpenAI, Groq, Together, a local vLLM, or your own class), register it
once in models.toml and summon it by name — no Ollama needed:
# one entry in models.toml (safe to commit — it names an env var, not the key):
# [my-model]
# provider = "openai" # the universal adapter
# base_url = "https://api.groq.com/openai/v1"
# model = "llama-3.3-70b-versatile"
# api_key_env = "GROQ_API_KEY"
export GROQ_API_KEY=... # the key lives in YOUR env, never the room
# then, in the TUI: /ai start my-model (or: hh-go ai my-model)Local and bring-your-own instances coexist in one room, each owned and scoped by
whoever summoned it. Full recipe — providers, explicit flags, discovery, and a
module:Class hook for a model you wrote — in the provider guide.
- Runs on your machine. The default provider is Ollama
— a local model (default
qwen2.5:3b), no API key, nothing leaves your host. Runhh/scripts/bootstrap-ai.shonce to install it and pull the model. - Addressed-only. The agent reads room traffic like any client but forwards
to the model only the messages that trigger it (
/ai …) — no passive surveillance, no cost or noise when idle. - Can drive the sandbox. Grant an agent drive (
/grant <name>, or summon it pre-granted with/ai start <name> allow) and ask it to act with/ai <name> !<task>. It works the shared box through a bounded host-side tool-calling loop — run shell commands, write and read files — inspecting each result before the next step, and you watch its commands appear live in the shared terminal. Every command runs inside the sandbox (the container/VM is the blast radius), capped in count and time. Without drive it stays advisory-only (it spells out the commands, runs nothing). Destructive commands are blocked pending an explicit/ai <name> confirm. - Model-agnostic. Swap the backend without touching the client: bundled
adapters for
ollama(default),anthropic, and any OpenAI-compatible endpoint (OpenAI, Groq, Together, local vLLM…), plus amodule:Classhook for your own. Cloud providers are opt-in and read their API key from the agent's environment — never the room. - Named profiles. Register a backend once in
models.tomland summon it by name:/ai start groq-llama. Profiles storeapi_key_env(the name of an env var, never the key), so the file is safe to commit. See the full provider guide — profiles, explicit flags, discovery, and bring-your-own-provider. - Discoverable.
/ai listshows who's present and/ai modelsshows what the active agent can serve (active model bracketed). With no agent running,/ai modelsstill probes your local Ollama so you can see what's pullable before summoning. By hand,--list-modelsenumerates a backend and--checkpreflights it (exit 0/1) without joining a room. - End-to-end like everything else. Replies are encrypted client-side; the server still only ever relays ciphertext.
Each agent uses one room seat — raise CMD_CHAT_MAX_USERS if the room is full.
To run an agent by hand (a cloud provider, or on another host), drive the bridge
directly:
.venv/bin/python -m cmd_chat.agent <server_ip> <port> \
--password <room-pw> --provider ollama --model qwen2.5:3b
# joins as its model tag ("qwen2.5:3b") unless you override with --name
# cloud (opt-in): --provider anthropic --model claude-opus-4-6 (needs ANTHROPIC_API_KEY)Seven bundled themes — crypt (default, neutral monochrome, ✝ sigil),
church, neon, blush, matrix, wraith, and goldcrypt. Switch live with
/theme <name>, list them with bare /theme, roll a fresh randomized vestment
with Ctrl+Alt+P (keep one you like with /theme save [name]), or load your own
TOML at launch with --theme <path> (see hh/themes/). Each theme defines its
own sigil, colours, and roster width.
The chat, roster (clergy), sandbox-terminal, and message-input panes are resizable and can be fullscreened — live, with no restart. Resizing the terminal also re-syncs the shared PTY grid so everyone in the room sees the same dimensions.
-
Fullscreen —
F4cycles the sandbox terminal fullscreen → chat fullscreen → back to the split. The input bar always stays visible, soF4always brings you back. -
Resize a pane — click a pane (or press
F5to cycle the selection: chat → terminal → roster → input). The selected pane gets a bold accent border and an✎marker in its title. The chat/terminal/roster panes form a binary split tree, so every pane resizes on both axes wherever a divider bounds it:↑/↓grow / shrink the selected pane's height. With a sandbox up, chat trades against the terminal directly below it. With no sandbox, chat and the clergy instead borrow from the message bar —↑grows the chat box (shrinks the input),↓does the reverse — so vertical resize always moves something. The input bar itself is also selectable and grows on↑/↓.←/→grow / shrink the selected pane's width (the chat/terminal column trades with the roster, down to hidden). Resizing the terminal's width now re-syncs the shared PTY too, not just its height.EscorEnterfinishes editing.
-
Presets — save the current arrangement and recall it later:
Command Effect /layoutShow the current arrangement + a reminder of the keys /layout save <name>Save the current split/roster to hh/layouts/<name>.toml/layout load <name>(or/layout <name>)Re-apply a saved layout /layout listList saved presets /layout rm <name>Delete a saved preset /layout resetRestore the default split
If the connection drops (network blip, laptop sleep), press Ctrl+R to re-run
the SRP handshake and re-attach — no restart needed. If you were hosting the
sandbox, it's re-announced so the room re-syncs the shared shell. Chat keeps up
to ~4000 lines of scrollback; the sandbox terminal keeps 2000.
Everything lives in hh/scripts/; run from the hh/ directory. Every script
supports -h / --help for full usage.
Setup
| Script | What it does |
|---|---|
bootstrap.sh |
One-shot first-run setup: checks prereqs, creates the Python venv, installs server deps, builds the Rust client. Baseline is non-interactive; add the AI layer with --ai. |
bootstrap-ai.sh |
Optional AI layer: runs the baseline, then installs Ollama and pulls a default local model for the /ai agent. |
install.sh |
Interactive setup wizard — walks build type, the AI layer (none / local / bring-your-own), and putting hh-go on your PATH. Calls bootstrap.sh. |
hh-go.sh |
The launcher/router: hh-go up|host|join|ai|device|share|status|down|install. Routes to the scripts + modules below; the installer can symlink it as hh-go. |
Run a session
| Script | What it does |
|---|---|
lets-hack.sh |
Local demo/test: boots a --no-tls server and tiles one TUI client pane per user in tmux. |
host-house.sh |
Host a real room and take a seat — builds the client, starts the server, and opens your own client, all in tmux. |
host-room.sh |
Host the server only (no seat): mints a password, frees the port, prints the LAN/Tailscale join banner, runs in the foreground. |
connect.sh |
Join a room with the password kept RAM-only (no-echo prompt). Flags: --sync (pull latest code before building), --tls, --insecure, --no-build, -P PORT. |
Sandbox provisioning — driven by the client at runtime; you rarely run these by hand
| Script | What it does |
|---|---|
ensure-docker.sh |
Install Docker and/or start its daemon (idempotent; --check/--plan/--yes). Invoked by /sbx docker. |
ensure-podman.sh |
Install Podman (rootless; sets up subuid/subgid). Daemonless — nothing to start. Invoked by /sbx podman. |
ensure-multipass.sh |
Install Multipass. Invoked by /sbx multipass. |
ensure-vbox.sh |
Install VirtualBox (warns on Secure Boot). Invoked by /sbx vbox and /sbx gui. |
vbox-new.sh |
Create + boot a fresh Ubuntu VirtualBox VM via cloud-init. Invoked by /sbx vbox new. |
sandbox-bootstrap.sh |
Baseline dev-tool install piped into a Docker sandbox at provision time (package list from sandbox-tools.json). |
sandbox-tools.json |
Editable package list consumed by sandbox-bootstrap.sh. |
Tests & demos — for contributors
| Script | What it does |
|---|---|
smoke-e2e.sh |
Headless CI smoke test: server + two real TUI clients in tmux; asserts SRP join, chat round-trip, /sbx dispatch. |
smoke.sh |
Crypto smoke test: Rust unit tests → SRP self-test → live server → Rust handshake → Python decrypts the Rust-sent message. |
test-features.sh |
Broad TUI regression: drives owner + member panes and scrapes the screen to assert ~13 UI features. |
demo-save-load.sh |
PoC harness proving the persistent-sandbox flow (/sbx save → quit → /sbx load, code intact). |
hh/scripts/archive/ holds personal demo-recording scripts (film-*.sh) that
depend on external video tooling — not needed to use or develop hack-house.
| Variable | Where | Effect |
|---|---|---|
CMD_CHAT_MAX_USERS |
server | Room capacity (default 4) |
PORT · PW · HOST |
scripts/lets-hack.sh |
Override the test server's port / password / bind host |
THEME |
scripts/lets-hack.sh |
Vestments for every pane (church · neon · crypt) |
HH_SESSION · HH_USER |
direnv autostart | tmux session name / your in-session name |
A room is one server process (cmd_chat.py serve <bind> <port> — or the
friendlier hh/scripts/host-room.sh / host-house.sh wrappers). What changes
between "modes" is the bind address and the front door — how guests reach
that process. The encryption is identical in every mode (SRP + client-side
Fernet; the server only ever relays ciphertext), so the choice is purely about
reachability and exposure, not trust.
| Mode | How you host | Who can reach it | Guest needs | Exposure |
|---|---|---|---|---|
| Loopback / local | serve 127.0.0.1 <port> --no-tls (or lets-hack.sh) |
only this machine | nothing | none — dev/testing, or the base for a Tor front door |
| LAN | serve 0.0.0.0 <port> (host-room.sh) |
anyone on the same network | your LAN IP + a client | your IP is visible on the LAN; no router setup |
| Tailscale (recommended) | serve 0.0.0.0 <port> --no-tls, hand out your tailnet IP |
anyone on your tailnet | tailnet access + a client | WireGuard-encrypted, no port-forward, no public exposure |
| Public internet | serve 0.0.0.0 <port> --cert … --key …, forward the port |
anyone with the address | a client | your real IP + an open port are public — use a real TLS cert |
| Tor onion | serve 127.0.0.1 <port> --no-tls --tor |
anyone with Tor | a client wrapped in torsocks |
no inbound port, host IP hidden, throwaway .onion, works behind CGNAT — see Tor onion hosting |
| Tor + browser (web relay) | --tor plus the web relay + cmd_chat.web publisher, fronted by a second onion |
a phone with just a browser | Orbot / Tor Browser (no install) | same as Tor, and the only mode a non-hack-house guest can join — see Browser access |
Notes that trip people up:
--tlsvs reachability are separate.--no-tlsis fine on Tailscale/Tor (the tunnel/onion already encrypts the transport); use a real cert only when you bind a plain public port. The room payload is end-to-end encrypted either way.- Inbound reach ≠ sandbox egress. The mode above controls how guests reach
the room; it never routes the summoned sandbox's outbound traffic — that's
HH_SBX_EGRESS(Sandbox egress), an orthogonal knob. - One-command deploy.
hh-go(hh/scripts/hh-go.sh) chains the modes above:hh-go host tailnet|lan|tor,hh-go sharefor browser access,hh-go upfor a local room + seat. The raw commands in each section are what it automates.
Mechanics for the modes above — pick per how you're reaching each other:
- Tailscale (recommended) — both parties join a tailnet; traffic rides an encrypted WireGuard tunnel, no port forwarding. Connect with
--no-tlsover the trusted tunnel, or keep TLS on. - LAN — use your local IP; both devices on the same network.
- Public internet — forward the port and use a real cert (
--cert/--keyon the server). - Tor (no network access needed at all) — see below.
Share the room password out-of-band (in person, a disappearing Signal message, or a one-time-secret link) — never over an unencrypted channel.
No inbound port, no Tailscale, no domain — the room gets a fresh, throwaway
.onion address that only exists for that one session. Good for a quick
one-off with someone you don't want to hand a real network path to, or hosting
from behind CGNAT/a coffee-shop network with zero setup on the router.
Requirements: tor installed on the host (sudo dnf install tor /
apt install tor); stem on the host (already in requirements.txt);
torsocks on the guest's machine only (the host doesn't need it).
# plain — talks to whatever tor ControlPort is already running (system tor,
# or a hardened instance you started yourself — see below)
python cmd_chat.py serve 127.0.0.1 9000 --password <pw> --no-tls --tor[tor] ephemeral onion service: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion:9000
That address is what you hand to a guest. It's minted fresh every run
(ADD_ONION, key never written to disk) and destroyed the moment the server
stops — nothing to clean up, nothing reusable across sessions.
serve --tor refuses to start on a non-loopback bind address by default
(pass --tor-allow-public-bind if you deliberately also want a plain public/LAN
listener at the same time — most people don't).
Recommended: sandbox the Tor process itself. scripts/tor-hardened-launch.sh
runs tor inside bwrap (no extra dependency — already on most Linux desktops)
with a Unix-socket Control/Socks port, cookie-only auth, and full (not
single-hop) onion mode, so a Tor daemon bug can't see your $HOME or other
processes:
scripts/tor-hardened-launch.sh
# prints: CONTROL_SOCKET=/run/user/1000/hh-tor-XXXXXX/control.sock
python cmd_chat.py serve 127.0.0.1 9000 --password <pw> --no-tls \
--tor --tor-control-socket /run/user/1000/hh-tor-XXXXXX/control.sockSame client either way (Rust hh or cmd_chat.py connect) — Tor is just the
transport, wrapped with torsocks so the connection routes through your local
Tor SOCKS proxy instead of dialing out directly.
Easiest: scripts/tor-onion-connect.sh. The raw commands below assume
you're sitting in a hack-house checkout root, which breaks the moment your
shell lands somewhere else. This helper finds a checkout regardless of your
current directory (checks $PWD, the script's own repo, then the usual
~/coding/hack-house/* spots — override with --repo/HH_REPO), prefers the
built Rust TUI, falls back to the Python client if that's not built, and
wraps either in torsocks for you:
scripts/tor-onion-connect.sh <onion-address> 9000 <name> --password <pw>
# works from anywhere — no need to cd into the checkout firstRaw form, if you'd rather not use the helper (must be run from a checkout root):
torsocks hh/target/debug/hack-house connect <onion-address> 9000 <name> --password <pw> --no-tls
# or:
torsocks python cmd_chat.py connect <onion-address> 9000 <name> --password <pw> --no-tlsYour own system tor needs to already be running (most Linux distros ship it
as a systemctl start tor away; torsocks talks to it on 127.0.0.1:9050 by
default — nothing else to configure).
Current limitation: the guest needs the external torsocks wrapper for
now — in-process SOCKS support (so --tor becomes a plain client flag, no
wrapper needed) is planned but not yet landed. Full design, the OPSEC posture
(why full onion mode not single-hop, why guard rotation is deliberately not
done, DataDirectory hygiene, the bind-address footgun this guards against) and
the roadmap live in docs/spec-tor-p2p-relay.md.
Everything above is terminal-only — the guest needs hack-house installed. For
someone who doesn't (a phone with nothing but a browser), pair Tor hosting
with the web relay — now shipped in main (web-relay/ + the
cmd_chat.web publisher), no separate branch or worktree to check out. Mint a
second onion service fronting the relay's port and hand out that link instead
of a Tailscale/domain front door.
# 1. the room itself, same as any --tor host
python cmd_chat.py serve 127.0.0.1 9000 --password <pw> --no-tls --tor
# 2. the relay (loopback only — its own safety gate refuses a public bind)
# + a publisher that joins the room, both from this checkout
( cd web-relay && ../.venv/bin/python -m relay --host 127.0.0.1 --port 8080 --no-tls & )
.venv/bin/python -m cmd_chat.web 127.0.0.1 9000 --password <pw> --no-tls \
--relay http://127.0.0.1:8080 --public-url http://<relay-onion-address> \
--label "hack-house room"
# 3. mint the relay's own onion, fronting :8080 — same tor daemon/ControlPort
# as step 1, `detached=True` so it survives this being a one-shot script
python - <<'PY'
from cmd_chat.tor.onion import EphemeralOnion
onion = EphemeralOnion() # or control_socket=... for the hardened launcher
service = onion.start(target_port=8080, virtual_port=80, detached=True)
print(service.address) # ← this is <relay-onion-address> for step 2's --public-url
PYHand the guest http://<relay-onion-address>/r/<slug>#k=<key> (printed by the
publisher). They need Orbot or Tor Browser — a stock Chrome/Safari cannot
open .onion links at all, no matter what's scanned; there's no way around
that, it's inherent to how onion services resolve. This is why it's presented
as an optional add-on to terminal hosting, not a replacement for it: the two
onions are independent (killing the room's tor daemon takes both down at
once, since they share it), and nothing about the terminal path above changes.
Both onions live on the same tor daemon/ControlPort — no second tor
process needed. hh-go share (hh/scripts/hh-go.sh) automates the relay +
publisher pair for you (the non-Tor case — hand out the link on LAN/tailnet, or
add the onion front above for a browser-only anonymous guest); then /share in
the TUI prints the URL to copy.
CLIENT SERVER CLIENT
│── POST /srp/init {A} ──────────►│ │
│◄── {B, salt, room_salt} ────────│ │
│ derive room_key = HKDF(password, room_salt) │
│── POST /srp/verify {M} ────────►│ │
│◄── {H_AMK, ws_token} ───────────│ │
│══ WSS /ws/chat?ws_token ═══════►│◄══════════════════════════════│
│ encrypt(msg, room_key) ───────►│──── ciphertext ──────────────►│
│ │ decrypt(ct, room_key) │
│ server stores ONLY ciphertext — it cannot read messages │
- SRP — both sides prove they know the password without transmitting it.
- Room key — each client derives
HKDF(password, room_salt)independently; the server never holds it. - Sandbox — the host runs a PTY locally and relays its output as encrypted
_sbxframes; drivers' keystrokes flow back the same way. Permissions are enforced both at the app layer (drive ACL) and in the VM (real unix users / sudo).
cd hh
cargo run -- selftest # offline: Rust SRP ≡ Python golden vectors
cargo run -- handshake <ip> <port> <name> --password <pw> --no-tlsSee CONTRIBUTING.md. Security reports: see SECURITY.md.
MIT · hack the planet
