shabadoo

Before you start

You need:

Understand the blast radius first.

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 PATHsudo mv shabadoo /usr/local/bin/ — or run ./shabadoo setup to install the toolchain into ~/bin.

macOS builds are unsigned.

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 … versionrun 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.

Why --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
No restart needed.

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:

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.

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

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