Skip to content

Repository files navigation

hack-house

encrypted collaborative terminal sessions with a summoned sandbox

License: MIT Rust Python 3.10+

hack-house demo

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.

Features

  • 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 / multipass box 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 vms detects 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 /kick a member (which also rotates the room password)
  • Local-first AI agent — /ai start summons 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 → /accept with 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 with web-relay for 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-tls for local/Tailscale use
  • Themes — seven switchable "vestments" (crypt default · church · neon · blush · matrix · wraith · goldcrypt), plus a live randomizer

Layout

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

Quick start

One line — copy, paste, enter:

git clone https://github.com/leetcrypt/hack-house.git && cd hack-house && bash hh/scripts/bootstrap.sh

That 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.sh

hh-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 down

Add the local AI agent (optional)

The 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 model

Cloud or custom models (Anthropic, OpenAI, Groq, vLLM, or your own) need no Ollama — see docs/providers.md.

Other bootstrap options

hh/scripts/bootstrap.sh --release    # build the client in release mode (faster)
hh/scripts/bootstrap.sh --check      # report tooling only, change nothing

Try it in tmux (scripts/lets-hack.sh)

The 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 down

Manual setup

Server (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/)

Autostart with direnv (optional, separate)

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 dir

After 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.

Using 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

The shared sandbox

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.

The physical device bridge

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-tls

Supported 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) and ssh_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 against ssh_conn for the verbs you need, following flipper.py.

Driving the shell

The shared terminal is watch-by-default: everyone sees the live output, but only granted drivers can type into it.

  • /drive (or F2) takes the keyboard; Esc releases it. /drive exists so the whole flow works on mobile/SSH clients with no function keys.
  • While driving, your keystrokes go to the PTY; Ctrl+C interrupts the running command (it does not quit the app).
  • PgUp/PgDn and the mouse wheel scroll the terminal's scrollback even while driving; End jumps back to live.

Unix permission control

Permissions are enforced at two layers:

  1. App-level drive ACL — who is allowed to type into the shared shell. The owner runs /grant <user> / /revoke <user>.
  2. Real VM identities — on multipass/docker, each member is provisioned an actual unix account, with the owner as superuser. On multipass, /sudo <user> / /unsudo <user> toggle real sudo rights 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.

Sharing files & directories

/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.

Share the room to a browser

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 room

Then, 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.

The AI agent (local-first)

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. Run hh/scripts/bootstrap-ai.sh once 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 a module:Class hook 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.toml and summon it by name: /ai start groq-llama. Profiles store api_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 list shows who's present and /ai models shows what the active agent can serve (active model bracketed). With no agent running, /ai models still probes your local Ollama so you can see what's pullable before summoning. By hand, --list-models enumerates a backend and --check preflights 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)

Themes (vestments)

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.

Window layout

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 — F4 cycles the sandbox terminal fullscreen → chat fullscreen → back to the split. The input bar always stays visible, so F4 always brings you back.

  • Resize a pane — click a pane (or press F5 to 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.
    • Esc or Enter finishes editing.
  • Presets — save the current arrangement and recall it later:

    Command Effect
    /layout Show 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 list List saved presets
    /layout rm <name> Delete a saved preset
    /layout reset Restore the default split

Staying connected

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.

Scripts

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.

Configuration

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

Hosting modes

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:

  • --tls vs reachability are separate. --no-tls is 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 share for browser access, hh-go up for a local room + seat. The raw commands in each section are what it automates.

Securing your connection

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-tls over 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/--key on 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.

Tor onion hosting

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).

Host

# 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.sock

Guest

Same 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 first

Raw 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-tls

Your 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.

Browser access for mobile (optional)

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
PY

Hand 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.

How it works

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 _sbx frames; 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).

Crypto parity

cd hh
cargo run -- selftest                              # offline: Rust SRP ≡ Python golden vectors
cargo run -- handshake <ip> <port> <name> --password <pw> --no-tls

Contributing

See CONTRIBUTING.md. Security reports: see SECURITY.md.

License

MIT · hack the planet

About

encrypted collaborative terminal sessions with a summoned sandbox

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages