Advanced setup
Everything here is optional. The quickstart gets you durable panes, the phone, your other machines through Arugula control, and agent blocks; these add web apps and VS Code beside your terminals, containers, and iTerm2. Open a port… and Open in editor say how to turn them on until they are.
Examples call the machine that serves the page home and the tailnet
<tailnet>.ts.net; use your own names.
- Service and logs
- Pane environment
- Claude Code hooks
- Browser blocks on ports
- Editor blocks
- More machines
- A container on the tailnet
- A box that can only dial out
- iTerm2, as a tmux client
- Labs
Service and logs
Section titled “Service and logs”arugulad install copies the binary to ~/.local/bin and installs a
service. Run it again to upgrade; the panes’ programs keep running through
it. Flags after -- are passed to the daemon on every start
(arugulad install -- --owner you@example.com).
- Linux:
~/.config/systemd/user/arugulad.service, enabled. With lingering (loginctl enable-linger $USER) it starts at boot, before you log in. Logs:journalctl --user -u arugulad. - macOS:
~/Library/LaunchAgents/arugulad.plist, which starts it at login and after a crash. Logs:~/Library/Logs/arugulad.log.launchctl bootout gui/$UID/aruguladstops it. Panes run your login shell from the user database (launchd sets no$SHELL); zsh and bash get shell integration. There’s no systemd FD store, so each pane’s shim keeps its terminal while the daemon is gone (--keep-panes, which the plist sets): a restart (launchctl kickstart -k gui/$UID/arugulad), an upgrade or a crash leaves the programs running, and the new daemon adopts them. Stopping it for good (launchctl bootout, logging out) ends them a minute later, if no daemon has come back. - macOS with no GUI login (a Mac you reach only over ssh, where that
user hasn’t logged in to the desktop since it booted): there’s no
gui/$UIDdomain, soarugulad installputs the same plist in the background session (user/$UID,LimitLoadToSessionTypeBackground). It keeps running after you log out, but after a reboot it doesn’t start until you log in to the desktop or runarugulad installagain (over ssh is fine;arugula --sshdoes it for you when the daemon isn’t running), and the install says so. - macOS, from boot:
arugulad install --systeminstalls a LaunchDaemon,/Library/LaunchDaemons/arugulad.$USER.plist, that runs the daemon as you from boot with nobody logged in. Run it as yourself, not as root: it runssudofor the two steps that need root and prints them first, so you need to be an admin. It replaces the LaunchAgent (one daemon per user). Later installs keep it;arugulad uninstallfirst to go back to an agent.sudo launchctl kickstart -k system/arugulad.$USERrestarts it. - Removing it:
arugulad uninstallstops and removes whichever is installed (the agent, the background agent, the LaunchDaemon with sudo, or on Linux the systemd user service). The binaries in~/.local/binand the state in~/.local/state/arugulastay. - From the desktop app: its Daemon menu (in the tray, and on a Mac in
the app menu and the Dock icon’s menu) says the daemon’s version, whether
it’s running and which service runs it (the app’s own launch agent,
io.arugula.desktop.daemon, or whatarugulad installset up), and whether and where this machine is joined to control. Restart, Stop… and Start go through that service, so a restart keeps the panes; a stop ends them, and the daemon stays stopped until Start or the next login (the LaunchDaemon asks for an admin password). Open log opens the log. When a newer daemon is out it offers the daemon’s own update (below); the app never replaces a running daemon itself. When the daemon and the app don’t speak a protocol in common, it says which is behind and opens the page that updates it.arugula statussays the same from a terminal. - Without systemd on Linux (a container, a box with another init): pass
--keep-panesfor the same behaviour.
Updates. At most every 12 hours the daemon asks where GitHub’s
releases/latest redirects to (one request, with nothing about you or the
machine in it) and keeps the answer in update-check.json in the state
directory. When it’s newer, the web client’s top bar offers it (GET /api/update says so too). The daemon updates itself when you ask: Update
now there, or arugulad update in a terminal (-y doesn’t ask). Either
downloads this platform’s archive from the release, checks it against the
release’s SHA256SUMS, and runs the new arugulad install, which keeps
your flags and restarts the service; panes keep running. If anything fails
before the restart, the old daemon carries on and says why (update.log in
the state directory). The button is for the service arugulad install
set up for you (install.sh, install.ps1 or the desktop app). Homebrew
installs, --system services (they need sudo or an administrator) and
daemons run by hand show the command instead. On macOS the desktop app’s
login item runs the daemon in the app, and an update goes to
~/.local/bin, which the app’s copy runs from then on, as long as it’s
newer. --no-update-check (ARUGULA_NO_UPDATE_CHECK=true) turns the
check off; a daemon run from where it was built (target/) doesn’t
check. The desktop app never replaces a running
daemon: the daemon it carries is for a machine that has none.
State (layout, logs, checkpoints) is in ~/.local/state/arugula, private
to you (0700/0600). --state-dir moves it.
Who gets in. On this machine, you: the CLI over its Unix socket (in
your private state directory), and anything on the TCP port that shows the
daemon’s local token (local-token in the state directory, made at
the first start, 0600). Loopback is shared by every account and program on
the machine, so being on it isn’t enough:
- Your browser gets the token as a cookie from a sign-in link:
arugula webopenshttp://127.0.0.1:7681through it (--printprints the link, to open by hand or through anssh -Lforward). Once per browser; it stays signed in until the token changes. A page opened without it says to runarugula web. The desktop app signs its own window in. - Programs send it as
Authorization: Bearer <token>(arugula --host http://127.0.0.1:7681does that for you). MCP clients usearugula mcp(the socket) or anarugula mcp token. - A new token signs every browser and program out: delete
local-tokenand restart the daemon.
Over the tailnet, the daemon asks tailscaled who each caller is and lets
in only the login that owns the node; --owner names someone else. Behind
tailscale serve, the identity serve adds is believed only from
tailscaled’s own connection (on Linux, the daemon checks which account
owns the other end). Tagged nodes, Funnel and the internet never get in.
Don’t put it behind anything else that would forward requests to it.
Pane environment
Section titled “Pane environment”On Linux, at boot the daemon starts before you log in, so its own
environment has no WAYLAND_DISPLAY, DISPLAY or desktop SSH_AUTH_SOCK.
Each new pane takes the systemd user manager’s environment as it is at that
moment, which your desktop session fills in at login. For variables every
pane should have from boot (PATH additions, EDITOR), put KEY=value
lines in ~/.config/environment.d/50-arugula.conf. Panes run $SHELL -l,
so your profile runs too.
Blocks that run your tools for you don’t have a
shell of their own, so the daemon reads your shell’s environment once, as
VS Code does: it runs $SHELL -l -i when it starts and keeps the PATH
and variables your rc files set, so node from mise or nvm is found there as
in a pane. If
your shell takes more than 10 seconds or fails, those blocks get the
daemon’s own environment, and the log says why. After changing an rc file,
arugula shell-env --refresh reads it again (arugula shell-env shows
what blocks get).
The names from before the rename still work: an ILLOGICAL_* setting counts
as its ARUGULA_* one when that isn’t set, and panes get ILLOGICAL_PANE
and ILLOGICAL_SOCK beside ARUGULA_PANE and ARUGULA_SOCK.
Claude Code hooks
Section titled “Claude Code hooks”Claude Code in a pane can tell you when it needs you, put its questions and
permission prompts on cards anyone on the team who may answer can answer,
and take follow-ups from them, all through hooks in
~/.claude/settings.json. The whole block, and what each part does, is in
the CLI’s Claude Code in a pane. Outside
an Arugula pane the hooks do nothing, so they’re safe everywhere.
Browser blocks on ports
Section titled “Browser blocks on ports”Open a port… (or arugula open :5173) shows a dev server beside its
terminal. Each block is served on an origin of its own by the daemon, so it
needs a listener and a wildcard name; without one they’re off, and Open a
port… says how to turn them on (this section).
The browser showing the block reaches that listener itself. Through Arugula control from another device (your phone, another computer) blocks aren’t relayed yet, so there they need the tailnet setup below.
On loopback only (no names, no certificates; the browser on the same machine):
arugulad install -- --block-listen 127.0.0.1:7701Blocks are http://b-<id>-<key>.localhost:7701; browsers resolve
*.localhost themselves. Each name carries a random key only the app and
the CLI know.
Over the tailnet (the phone, other machines):
- A domain you control in Cloudflare. Add a DNS-only (grey cloud)
Arecord*.arugula.example.compointing at the host’s tailnet address (tailscale ip -4). Only the tailnet can reach that address. - A Cloudflare API token with Zone › DNS › Edit on that zone, in a file.
The daemon uses it for Let’s Encrypt DNS-01 challenges: it gets the
wildcard certificate itself and renews it two thirds of the way through
its life (kept in
<state>/acme/). - Pick a free port on the tailnet address (443 is usually
tailscale serve’s):
arugulad install -- --block-listen 100.x.y.z:7443 \ --block-domain arugula.example.com \ --block-acme-cloudflare-token-file ~/.config/arugula/cloudflare-token--block-acme-directory staging uses the test CA while you try it;
--block-cert/--block-key serve a certificate you renew yourself.
Callers are checked with tailscale whois: only the owner gets in.
Editor blocks
Section titled “Editor blocks”Open in editor and arugula edit run VS Code (code-server) and show it
like a browser block on a port, so they need block sites
(--block-listen, above); without them Open in editor says how to turn
them on.
- code-server: the release Arugula pins is downloaded the first time
an editor opens (about 230 MB) into
$XDG_CACHE_HOME/arugula/code-serverand checked against its SHA-256.--code-server PATHruns another one instead (a recent one: Arugula passes--idle-timeout-secondsand--socket-mode). - Where things are: settings, extensions and VS Code’s state in
<state>/editor/(user/User/settings.jsonis yours after the first start), its log in<state>/editor/code-server.log, its socket beside the CLI’s (<sock>-code, mode 0600). code-server keeps its own logs in~/.local/share/code-server. - Stopping:
--editor-idle SECONDS(default 900, at least 60) after the last window closes. It runs in a scope of its own, so restarting the daemon leaves it, and open windows reconnect.
More machines
Section titled “More machines”Install Arugula on each machine (the desktop app, install.sh or
Homebrew), then add it to your account on
Arugula control:
arugulad join https://control.arugula.ioApprove the code it prints on a signed-in device. Every machine you join shows in the host menu of control’s page and of the desktop app, with its sessions and tabs, on every device you’ve added; nothing has to be wired from one machine to another. Getting started’s Cloud step does the same with a button. control.md has the rest: teams, phones, leaving, moving a machine.
The CLI still reaches only the daemon on its own machine, or one on the
tailnet with arugula --host NAME … once that daemon’s list has it
(arugula hosts add NAME https://NAME.<tailnet>.ts.net).
A container on the tailnet
Section titled “A container on the tailnet”A container or box without systemd can run the static Linux binaries from a
release. Copy arugulad and arugula into it, then:
arugula hosts invite # on home: prints a tokenarugula install --tailnet file:KEYFILE \ --home https://home.<tailnet>.ts.net --join TOKEN # in the containerThe key is an ephemeral, tagged Tailscale auth key (e.g. tag:container), in
a file (or - for stdin; never on a command line). This downloads
tailscaled if it isn’t there, runs it in userspace mode with its own state
in ~/.local/state/arugula-sandbox, joins, puts the daemon behind
tailscale serve, and adds it to the home daemon’s list. arugulad sandbox keeps tailscaled and the daemon running; after a reboot, run
arugulad sandbox & again. Without --join it prints the arugula hosts add line to run on home.
A box that can only dial out
Section titled “A box that can only dial out”For a box that allows nothing in but outbound HTTPS. On home,
arugula hosts token sbx (or hosts invite); on the box:
arugulad --peer wss://home.<tailnet>.ts.net --token ~/.config/arugula/host-token \ [--join INVITE] [--sync [--sync-live]] &The home daemon must be reachable at that URL from the box and accept
its name as a Host (--public-host). The home daemon lists the box and
answers for it at /h/sbx/…, so the host switcher and arugula --host sbx work as for any host. --sync pushes closed panes’ history to the
home daemon, encrypted at rest there; --sync-live pushes open ones too.
iTerm2, as a tmux client
Section titled “iTerm2, as a tmux client”iTerm2’s tmux integration works with Arugula in place of tmux: sessions are sessions, tabs are native windows, splits are native splits, and the same layout stays live in the browser. From iTerm2:
ssh -t home '~/.local/bin/arugula tmux -CC attach' # the first sessionssh -t home '~/.local/bin/arugula tmux -CC attach -t work'ssh -t home '~/.local/bin/arugula tmux -CC new -s ipad'-t is a session name or $N; plain arugula tmux -CC is attach. To
detach, use Shell › tmux › Detach. Add --host NAME before tmux to
reach another daemon. The CLI behaves as arugula tmux when it is
called tmux, so for tools that run tmux -CC by name, put a link where
only they look (mkdir -p ~/.local/share/arugula/tmux && ln -s ~/.local/bin/arugula ~/.local/share/arugula/tmux/tmux, then ssh -t home 'PATH=~/.local/share/arugula/tmux:$PATH tmux -CC attach'), not on
your PATH, where it would hide the real tmux.
- It reports tmux 3.5a. Typing, splits, divider drags, window resizes, new tabs and closing panes change the daemon’s layout, which the browser shows at once, and the other way round.
- The window’s size follows whoever claimed it last: iTerm2 when it resizes a window or you type in it, the browser when you click or type there.
- Agent and browser blocks show as read-only panes with their text and a note to open them in the web app.
- If iTerm2 falls behind a fast pane it shows “paused”; unpausing re-captures the pane and carries on.
A manual test script, and how to record the conversation, are in development.md.
A file named labs in a machine’s state directory turns on what a new
install doesn’t show: chat and threads, huddles, Fountain, studio apps,
chant workspaces, VM tabs and sandboxes, ssh invites for guests, the swarm’s
city, hive and timeline views, and the matching tools, commands and options
of arugula mcp, arugula --help and arugulad --help. They all keep
working without it; they just aren’t offered.
touch ~/.local/state/arugula/labsThe state directory is $ARUGULA_STATE_DIR if set, else
$XDG_STATE_HOME/arugula, else ~/.local/state/arugula; on Windows,
%LOCALAPPDATA%\arugula\state. The file can be empty. Delete it to turn
labs off again.
- Per machine. It’s read by the machine that serves the page, so every machine you want them on needs its own. Someone you share a session with sees chat and huddles on your machine if it has labs, and not otherwise.
- No restart. The daemon looks for the file whenever it’s asked; reload the page to see the change.
- Each feature still needs its own setup. Fountain needs a login, studio a link, VMs a sandbox provider, guest ssh its listener; labs only stops them from being hidden.