Before you start
You need:
- tmux and the Claude Code CLI on every machine you want to see.
- One always-on host for the coordinator. A small VM, a NAS, a Pi, a container — it only needs to be reachable by the others.
- A private network between them — a VPN or a tailnet — or, if the coordinator has to be reachable from the internet, an identity proxy in front of it. Never a bare public port.
Anyone who can reach your coordinator with a credential can send keystrokes into panes
running claude --dangerously-skip-permissions. That is the feature, and it
is also the risk. Read the safety section before you expose it
to anyone but yourself.
1. Get the binary
One static binary, no runtime, no dependencies. Published for every tagged release and built by CI from the tag:
OS=$(uname -s | tr '[:upper:]' '[:lower:]') # linux | darwin ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/') BASE=https://github.com/alexj212/shabadoo/releases/latest/download curl -fsSL -o shabadoo "$BASE/shabadoo-$OS-$ARCH" curl -fsSL -o SHA256SUMS "$BASE/SHA256SUMS" # Verify before running it. The whole point of a published checksum is that # somebody checks it, and this is a binary you are about to give a terminal. grep " shabadoo-$OS-$ARCH$" SHA256SUMS | sed "s/shabadoo-$OS-$ARCH/shabadoo/" | sha256sum -c - chmod +x shabadoo && ./shabadoo version
sha256sum is shasum -a 256 on macOS. Put it on your
PATH — sudo mv shabadoo /usr/local/bin/ — or run
./shabadoo setup to install the toolchain into ~/bin.
curl does not set the quarantine attribute, so the commands above work as
written. A binary downloaded through a browser is quarantined and Gatekeeper will
refuse to run it — clear it with xattr -d com.apple.quarantine shabadoo.
There is deliberately no curl … | sh installer: this binary installs a
toolchain, and a project whose own rule is never fetch from the network during
install should not open by asking you to pipe an unread script into a shell.
Or build from source, which needs only a Go toolchain:
git clone https://github.com/alexj212/shabadoo && cd shabadoo
make build # stamps the version from git describe
Prefer a container for the coordinator?
Multi-arch, about 27 MB, static binary on Alpine as UID 1000. Coordinator only — an agent drives its host's tmux, so there is nothing to containerise.
docker pull ghcr.io/alexj212/shabadoo:0.1.4
Pin the version, even though latest does track the newest release. This
image drives every pane on every connected machine, so "whatever was pushed most recently"
is not something a restart should pick up on its own. Check what you actually got with
docker pull before docker run … version — run reuses
a cached image and will happily report an older version than the registry holds.
The examples directory
has a working compose
file, a TLS
variant, and a systemd
unit for running an agent by hand.
2. Run the coordinator
On the always-on host. It needs a listen address, somewhere to keep its database, and a file listing which agents may connect. That file starts empty — it is what every coordinator looks like before its first machine — and is re-read when it changes, so adding an agent in step 4 needs no restart:
mkdir -p ~/.config/shabadoo && cd ~/.config/shabadoo
touch authorized_agents # empty; agents get added in step 4
shabadoo hub \
--addr 0.0.0.0:8787 \
--db hub.db \
--agents authorized_agents \
--device-tokens \
--bootstrap
It logs the port, the database, how many agents are authorised and how many devices are
enrolled — and, because of --bootstrap, a single-use pairing code valid for
five minutes. That code is step 3. With no agents yet it says so plainly, because a
dashboard reporting "No agents connected" with nothing to explain it is its own trap.
--device-tokens means every human client — browser, phone, CLI — must present
a token it was enrolled with. There is no login form and no anonymous access.
--bootstrap, and why you remove it.
Enrolling the first device is circular: minting a pairing code requires an already
enrolled credential. --bootstrap breaks that once by printing a single-use
code to the log. Remove the flag after the first pairing — it mints a fresh code
on every start.
The coordinator refuses to start without an auth posture. That is deliberate: the posture that would "just work" is the absence of authentication.
3. Pair yourself
Take the code from the coordinator's log and redeem it:
shabadoo pair --code ABC12345 --coord https://coordinator.example
That stores a token at ~/.config/shabadoo/cli_token and remembers the
coordinator URL, so later commands need neither.
Now open the dashboard in a browser at your coordinator's address. It will redirect you to the pairing page; mint a code for it from the CLI:
shabadoo pair --label "laptop browser"
Or, once a browser is paired, use the Devices panel in the dashboard — it shows a QR code, which is the fastest way to enrol a phone.
For anything that only needs to watch, enrol it read-only:
shabadoo pair --label "wall display" --scope read
4. Connect a machine
On a machine that has tmux and Claude Code. First give it an identity — an SSH key it uses to authenticate to the coordinator:
ssh-keygen -t ed25519 -N "" -C laptop -f ~/.config/shabadoo/agent_key
Install the agent as a service, pointed at your coordinator:
shabadoo setup --service --coord https://coordinator.example
Then authorise it on the coordinator by appending its public key, with the node name as the comment:
# the agent machine prints exactly this line during setup
printf '%s %s\n' "$(cut -d' ' -f1,2 ~/.config/shabadoo/agent_key.pub)" laptop \
>> ~/.config/shabadoo/authorized_agents
The file is re-read when it changes. Restarting to admit one agent would disconnect every other one.
Within a few seconds the machine appears in shabadoo sessions and on the
dashboard. It dialled out — you did not open a port on it.
5. More machines
Repeat step 4. Set a short, distinctive host label first, because it names the node, the tmux windows and the remote-control alias:
# ~/.config/claude/env
export CLAUDE_HOST_LABEL=laptop
Start sessions with the built-in launcher, which names windows consistently so the dashboard can match a folder to its window:
cd ~/projects/thing && shabadoo attach
The operator CLI
The same API the dashboard uses, from a terminal — and authenticating the way the mobile client does, so that path is exercised daily rather than first tried on the day it ships.
shabadoo sessions # every node; ! marks one waiting on a prompt shabadoo tail homelab # what is on that pane right now shabadoo send --pane iptv "run the tests" shabadoo keys --pane iptv Enter # answer a dialog shabadoo command --pane homelab /clear shabadoo open techtalks # start a session in a folder shabadoo kill homelab # asks first shabadoo audit # who drove which pane shabadoo devices # enrolled clients
Panes are addressed by name — exact match first, then substring, and an ambiguous
name is an error rather than a guess. Prefer it: window indices shift as sessions come and
go, so a number in a script is a stale pointer waiting to type into the wrong project.
--window N still works everywhere the name does.
Notifications
When a session sits at a prompt for 90 seconds, the coordinator notifies you. Point it at an Apprise endpoint and it will reach Telegram, Pushover, ntfy or anything else Apprise supports:
shabadoo hub ... --apprise-url http://apprise:8000/notify/all
The policy matters more than the mechanism:
- 90 seconds of grace — most prompts are answered by whoever is already at the keyboard, and a stream that is mostly noise gets muted.
- Edge-triggered — agents report every few seconds; notifying on the state rather than the transition would be a notification forever.
- One reminder an hour while it still stands.
- Answering resets it, so blocking twice in a morning is two events.
Sessions working together
Give each Claude session the MCP server and they can reach each other through their host's agent — no credential of their own required:
claude mcp add shabadoo -- shabadoo mcp
Each session then gets tools to list its peers, message them by project, drain its own inbox, say what it is currently working on, and notify you.
# from inside one session, addressed by domain — not by session id
session_send to: "homelab" title: "cert expiring" body: "..."
session_status_set status: "waiting on the homelab peer"
Upgrading
Publish binaries to the coordinator once, then have each node replace itself:
make dist && shabadoo publish dist/ shabadoo upgrade --all
Deliberately operator-triggered rather than automatic, and one node at a time: a bad build pushed everywhere at once has to be recovered by SSHing to every machine, which is the dependency dial-out agents exist to remove. Each node verifies a checksum and runs the staged binary before swapping, and keeps the previous one alongside.
Auth postures
The coordinator requires exactly one and refuses to start without it.
| Flag | Use |
|---|---|
--device-tokens |
The default choice. Every client presents an enrolled token. Pair the first
with --bootstrap. |
--tailscale-allow |
On a tailnet, identify people by their tailnet login — no pairing at all. Only the logins you list are admitted; membership alone is not enough. Requires the coordinator to terminate the connection itself, not sit behind a reverse proxy. |
--access-team / --access-aud |
Cloudflare Access, when the origin is reachable only through the tunnel. |
--insecure-no-access |
Local development only. Refuses any non-loopback address. |
Device tokens last 90 days. Clients renew themselves automatically — the coordinator reports the remaining lifetime on every response — so in practice you never touch it.
Where things live
| Path | What |
|---|---|
~/.config/shabadoo/hub.db |
Coordinator state: enrolled devices, sessions, mail, audit log. Back this up — losing it signs out every paired client. |
~/.config/shabadoo/authorized_agents |
Which machines may connect. A trust decision, so it is never written for you. |
~/.config/shabadoo/agent_key |
That machine's identity. Keep it and a reinstall rejoins without re-authorising. |
~/.config/claude/env |
Per-host launcher settings: host label, Claude flags. |
Uninstalling
shabadoo uninstall --dry-run # what would go shabadoo uninstall # the services shabadoo uninstall --all # ...and the binary
It removes what setup generated and nothing else. Your Claude config and the per-host env
file are never touched, and coordinator state is kept unless you pass
--purge.
Troubleshooting
An agent will not connect
Check its public key is in authorized_agents on the coordinator, with the node
name as the comment. Then check its log — the agent reports the exact reason and retries
with backoff.
The dashboard says "No agents connected"
GET /healthz on the coordinator is unauthenticated and reports how many agents
are attached. If it says zero, the problem is agent-side; if it says more, the problem is
your credential.
Sessions appear but the list is empty
The agent runs as a service, and service managers do not give it your shell's environment.
If it cannot find tmux, or has no locale, it can connect and still report
nothing.
Commands succeed but nothing happens in the pane
A dialog owns the keyboard. When Claude Code has a modal up, typed text is discarded and
Enter is consumed by the dialog. shabadoo detects this and refuses the send rather than
reporting a success that did nothing — use shabadoo keys to answer it.
Everything is behind a reverse proxy and updates stop
Agents hold a long-lived text/event-stream open. A buffering or compressing
middleware presents as agents connected, commands never arrive — a silent failure
rather than an error. Exclude that route from any such middleware.