Docs › Adding a worker
Adding a worker
Easiest: pair with a code. In the app, Add a machine → Pair with a code, pick the kind of machine, and run the one command it shows on the new machine (Terminal on a Mac or Linux, PowerShell as administrator on Windows). It installs what's missing, lets the hub's key in (only from the hub's addresses, for running commands and for tunnels to the machine's own services) and shows a check code; confirm on the hub that both codes match. For a Linux server, choose Server → Step by step instead: it gives you one command to run as an administrator, then a check code to compare. (On Windows, if the Linux part fails, the script puts back what it changed on the PC, so you can fix the cause and run the same command again.)
The app also does this step by step: tap Add a machine in the Sparehand app. It shows the steps below for your machine's OS with the hub's key and address filled in, checks the machine's fingerprint, adds it and helps you lock it down. This page is the same process by hand, with more background.
A worker is any macOS, Linux or Windows (WSL2) computer that the hub can reach over SSH with a key. Sparehand installs nothing permanent on it: jobs run in ~/.sparehand/runs.noindex/, and a few small state files live in ~/.sparehand/.
Adding one takes three steps:
- Get SSH working from the hub with a key
- Prepare the machine: macOS, Linux, Windows (WSL2) or a server, at home or anywhere
- Run
sparehand add
Then lock it down so only the hub can log in.
The examples use 192.168.1.10 for the hub and 192.168.1.20 for the worker; use your own. Give both fixed addresses (a DHCP reservation in your router), because the hardening below allows logins from the hub's address only.
1. SSH from the hub with a key
On the hub, make Sparehand's own key (once; the app's Setup does the same). It is ~/.ssh/sparehand_ed25519, without a passphrase because jobs run with nobody there to type one, readable by you only, and kept apart from your own keys, so removing a worker removes only this one from it. The command prints the line a worker's ~/.ssh/authorized_keys takes:
sparehand ssh-key
Sparehand offers this key first on every connection (add, doctor, pairing, jobs). Without it, it uses your default key in ~/.ssh (id_ed25519 and the like), as before.
Give the worker a short name in ~/.ssh/config on the hub. Sparehand always uses this name, so if the worker's address changes you only edit it here:
Host worker1
HostName 192.168.1.20
User alice
IdentityFile ~/.ssh/sparehand_ed25519
Copy the public key to the worker (it asks for the worker's password one last time):
ssh-copy-id -i ~/.ssh/sparehand_ed25519.pub worker1
If ssh has never connected to that address, it asks you to confirm the machine's fingerprint. Compare it with what the worker itself prints (ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on it); Sparehand can show and record it too:
sparehand hostkey 192.168.1.20 # the fingerprints that address offers
sparehand trust 192.168.1.20 SHA256:… # only after comparing it on the worker
Then check that it logs in without a password prompt:
ssh -o BatchMode=yes worker1 true && echo ok
2. Prepare the machine
Every worker needs bash, rsync and git, plus whatever your projects use (Node, Python with uv, Rust…). Sparehand installs project dependencies itself (npm, pnpm, yarn, bun, uv) when a lockfile changes, but not the tools.
The account Sparehand logs in to needs an sh-compatible login shell (bash, zsh or sh; fish won't work), since Sparehand sends it short shell commands.
SSH sessions don't load your shell profile. Sparehand adds /opt/homebrew/bin, /usr/local/bin, ~/.local/bin, ~/.cargo/bin and ~/.bun/bin to PATH. For anything else, create ~/.sparehand/env on the worker; Sparehand sources it before every job:
# ~/.sparehand/env on the worker
export PATH="$HOME/.nvm/versions/node/v22.11.0/bin:$PATH"
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
macOS worker
- System Settings → General → Sharing → Remote Login: on, and allow access for your user only.
- Install the tools, e.g. with Homebrew:
brew install git node(macOS shipsbashandrsync). - Keep it awake while it's plugged in: System Settings → Battery → Options → Prevent automatic sleeping when the display is off (laptops) or Energy → Prevent automatic sleeping.
- Optional but worth it: add Terminal and
sshd-keygen-wrapperto Privacy & Security → Developer Tools, and your code folders to Spotlight → Search Privacy, so macOS doesn't scan every file a build writes. (Sparehand's run folder ends in.noindex, which Spotlight already skips.)
Two Macs can also be joined with a Thunderbolt cable ("Thunderbolt Bridge" network, give both ends a fixed address like 10.0.0.1 / 10.0.0.2): syncing a checkout is then almost instant.
Linux worker
sudo apt install openssh-server rsync git # Debian/Ubuntu; use your distro's package manager
sudo systemctl enable --now ssh
Install your project tools (Node, uv, …) for the same user you log in as. taskset (from util-linux, usually present) lets Sparehand pin a job to the CPUs you allow with --cpus.
Windows worker (WSL2)
Jobs run inside a WSL2 Linux distribution (Ubuntu here). Windows itself only needs to keep WSL running and let the hub's SSH connection in.
In PowerShell (as administrator):
wsl --install -d Ubuntu-24.04
In Ubuntu (start it from the Start menu once to create your user):
sudo apt update && sudo apt install -y openssh-server rsync git
# let systemd start sshd with the distribution
printf '[boot]\nsystemd=true\n' | sudo tee /etc/wsl.conf
**.wslconfig** in your Windows user folder (C:\Users\<you>\.wslconfig) decides how much of the PC WSL may use, and lets the hub reach WSL directly on the PC's own address:
[wsl2]
networkingMode=mirrored # WSL shares the PC's IP address, so the hub can SSH to it
processors=12 # how many CPU threads WSL may use
memory=16GB # how much RAM WSL may use
swap=8GB
vmIdleTimeout=-1 # don't shut WSL down when no window is open
[experimental]
autoMemoryReclaim=dropCache # give memory back to Windows after big builds
Apply it with wsl --shutdown (then start Ubuntu again). If the PC has 32 GB, giving WSL about three quarters of it leaves Windows comfortable; Sparehand's memory checks keep jobs within what WSL has.
Keep WSL running after you log in, even with no terminal open: create a scheduled task that starts it in the background (PowerShell as administrator, adjust the distribution name):
schtasks /create /tn "Start WSL" /sc onlogon /rl highest /f `
/tr "conhost.exe --headless wsl.exe -d Ubuntu-24.04 --exec sleep infinity"
schtasks /run /tn "Start WSL"
Let the hub in through the firewall. With mirrored networking, WSL traffic goes through the Hyper-V firewall. Block everything inbound by default and allow SSH from the hub only (PowerShell as administrator; 40E0AC32-… is the fixed ID of WSL):
$wsl = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'
Set-NetFirewallHyperVVMSetting -Name $wsl -DefaultInboundAction Block
New-NetFirewallHyperVRule -Name SparehandSSH -DisplayName "Sparehand: SSH from the hub" -Direction Inbound `
-VMCreatorId $wsl -Protocol TCP -LocalPorts 22 -RemoteAddresses 192.168.1.10
Gaming PCs: if the PC has an NVIDIA GPU, Sparehand sees its load through WSL (/usr/lib/wsl/lib/nvidia-smi). While the GPU stays busy (someone is playing), the PC takes no jobs, and a job already running there is stopped and rerun elsewhere. You can also pause the PC from Windows itself with two desktop shortcuts:
:: Pause Sparehand jobs.bat
wsl.exe -d Ubuntu-24.04 -- sh -c "mkdir -p ~/.sparehand && touch ~/.sparehand/paused"
:: Resume Sparehand jobs.bat
wsl.exe -d Ubuntu-24.04 -- sh -c "rm -f ~/.sparehand/paused"
A degraded GPU bridge. WSL reaches the card through a paravirtual bridge (dxgkrnl), and after a few days up that bridge can degrade: copies from memory to the card drop from about 8 GB/s to 0.1 GB/s while the card itself stays fast, so a Studio picture that took 20 seconds takes 5 to 10 minutes (loading the models is the slow part; the sampling is fine), and a local AI model loads just as slowly. Nothing shows in Task Manager, and wsl --shutdown doesn't help; a Windows restart does (one laptop measured 0.10 GB/s after 56 hours up, and 9.3 to 10.6 GB/s after a restart; healthy readings there are 7.8 to 10.6). Sparehand watches for it on every Linux and WSL worker with an NVIDIA card, with a small probe it puts there (~/.sparehand/gpu-probe.sh, from lib/worker-gpu-probe.sh): a 256 MB copy from pinned memory to the card and one on the card, with the PyTorch that Studio's ComfyUI (or Chatterbox) brought, measured at most every 10 minutes. Under 1 GB/s to the card is degraded (SPAREHAND_MIN_H2D). Then:
sparehand run --gpupasses the machine over, with the reason in the usual "every worker is busy" line (GPU bridge degraded (0.10 GB/s)), so the job goes elsewhere or waits; CPU jobs still go there. A GPU job whose chosen machine hasn't been measured yet measures it first (a few seconds).sparehand status, the Machines tab and the menu bar show it (GPU bridge degraded, 0.10 GB/s, restart Windows), with a notification and an Advisor recommendation;sparehand heal NAME --checkprints the numbers and what the rules below would do.sparehand studio statusandcomfy.py doctorprint them too (h2d_gbs,d2d_gbs,dxg_errors).- Healing, per worker:
sparehand set NAME heal auto|ask|off(or Machines → Edit). Withask, the default, its card gets a Restart Windows now button (sparehand heal NAMEfrom the terminal), refused only while a job runs there. Withauto, Sparehand restarts Windows by itself (shutdown /r /t 30, with a message on the screen) once the machine is idle: no Sparehand job on it, its GPU not busy, not paused, its apps closed (worker mode on, or nothing left for it to close), and nobody has touched the keyboard or mouse for 10 minutes (Windows' own record,SPAREHAND_HEAL_IDLE_MIN); at most once in 6 hours (SPAREHAND_HEAL_EVERY), and never while a job runs. It waits up to 10 minutes for the machine to answer again (a PC that signs in by itself is back on SSH in one to three minutes), measures again, and the Activity tab logs Restarted NAME to heal its GPU bridge: 0.10 → 9.45 GB/s, with a notification. A plain Linux worker gets the badge only; nothing restarts it. - A blunt rule instead, or as well:
sparehand set NAME restart-at 05:00restarts Windows every day in the hour after that time, under the same idle rules.
The kernel log's dxgkio_escape: Ioctl failed lines (dxg_errors) are shown for the record, but a healthy machine has dozens to hundreds of them minutes after a restart, so the speed is what counts and nothing is decided on that count. If the bridge degrades often, two things on the Windows side help: keep the NVIDIA driver current (GeForce or Studio, from nvidia.com, not Windows Update's), and keep hardware-accelerated GPU scheduling on (Settings → System → Display → Graphics → Default graphics settings), then restart once more.
A PC that also starts Windows (dual boot)
A gaming PC or laptop can run Linux from its own disk or partition for Sparehand and keep Windows for everything else. Jobs get the whole machine there (no WSL memory limit, no GPU bridge to degrade), and Sparehand switches it between the two from the hub.
- Linux side: add it as a Linux worker. It also needs
efibootmgrand passwordless sudo forefibootmgrandshutdown, for its user:echo "you ALL=(root) NOPASSWD: /usr/bin/efibootmgr, /usr/sbin/shutdown" | sudo tee /etc/sudoers.d/sparehand-switch. - Windows side: turn on OpenSSH Server (Settings → System → Optional features, or
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0in an administrator PowerShell), set its service to start automatically, and put the hub's public key inC:\ProgramData\ssh\administrators_authorized_keysfor an administrator account (readable only by Administrators and SYSTEM:icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant Administrators:F /grant SYSTEM:F). Windows' SSH answers at the sign-in screen too, so nobody has to sign in there. - On the hub: a
Hostentry in~/.ssh/configfor its Windows side. When both systems answer at the same address, give it its ownHostKeyAlias(HostKeyAlias myworker-windows) so SSH keeps the two host keys apart. Then tell Sparehand:
sparehand set myworker windows myworker-windows
From then on the machine's card has Restart into Windows… (under More; the Mac app: in its ⋯ menu), and while it runs Windows the card says In Windows, gives it no jobs, and has Restart into Linux. In a terminal: sparehand switch myworker windows and sparehand switch myworker linux. Either way it is refused while a job runs there (Pause sends jobs elsewhere first). From Linux it restarts at once; from Windows, Windows shows its own 30-second countdown to whoever is at it.
The switch chooses the other system for the next start only (the firmware's one-time BootNext, bcdedit's bootsequence), so the firmware's default boot order stays as it is: switching the machine on starts what it always did. On Linux that is the entry called Windows Boot Manager; on Windows, the firmware entry named after the Linux distribution (ubuntu, debian, fedora, …).
Server (at home or anywhere)
Use a spare Linux server, not one that serves a website or app: builds would compete with it. Sparehand uses a separate build user. Only the hub's key for that user is limited; your own logins stay as they are.
- Reach it privately. For a server outside your home, use Tailscale on the hub and server. Use the server's Tailscale name, like
server.tail1234.ts.net. This is better than opening SSH to the internet. A domain name likebuild.example.comor a public IP works too if the hub can reach its SSH. - Open Add a machine → Server → Step by step in the dashboard. Enter a name, the server's address and a build user (default:
sparehand). - Paste the one setup command on the server as an administrator (root or a user that can sudo). It installs missing rsync, git and zsh, and the SSH server if needed. It creates the build user if needed and adds the hub's key with limits on forwarding and terminal access. If an existing
AllowUsersorAllowGroupsrule blocks the build user, it offers to add the user or its group to that rule. Install project tools (Node, Python with uv, …) so the build user can run them, for example system-wide with the package manager. - Compare the check codes printed on the server and shown in the dashboard. Confirm they match, then press Add. If they differ, don't add the server.
- Lock it down in the last step. Limit the key to the address the server sees the hub coming from. You can include other hub addresses too. This uses
sparehand lock; it checks a fresh SSH connection and undoes the limit if the hub cannot log in. If setup already limited the key to the hub's addresses, the last step just checks the connection.
Without the dashboard: install rsync, git, zsh and an SSH server, create the build user and copy in the hub's public key. Allow the build user through any SSH access rules and install its project tools. On the server, run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub. Compare that fingerprint with what the hub shows, then trust and add it:
sparehand hostkey build.example.com
sparehand trust build.example.com SHA256:… # only after comparing on the server
sparehand add server sparehand@build.example.com
sparehand lock server
sparehand lock NAME [ADDRESS...] limits the hub's key in the build user's ~/.ssh/authorized_keys. Without addresses, it uses the address the server sees for the hub. To choose addresses yourself, use e.g. sparehand lock server 192.168.1.10. It leaves other keys and the server's SSH settings alone.
If the hub's public IP changes, the limit can lock it out. Log in to the server as an administrator and remove the from="…" part (and its following comma) from the hub's key line in the build user's ~/.ssh/authorized_keys. Then run sparehand lock server on the hub again. Tailscale avoids this problem because its addresses stay the same.
3. sparehand add
On the hub:
sparehand add worker1 worker1
The first worker1 is the name Sparehand shows; the second is the SSH host (the alias from ~/.ssh/config). sparehand add logs in, reports the OS, CPU count, memory, tools and GPU it finds, and adds a line to ~/.config/sparehand/workers. Without --jobs, it suggests the job slots from the machine's CPUs and memory: about 12 GB and 8 CPUs per job, at most 4. With more than one job, each job gets about 70% of the CPUs shared out, so whoever uses the machine keeps some. A 12-CPU, 16 GB MacBook gets 1 job with all its CPUs; a 24-thread PC with 26 GB gets 2 jobs of 8 CPUs. Change it any time with sparehand set NAME jobs N and sparehand set NAME cpus N. Options:
| Option | Meaning |
|---|---|
--jobs N | How many Sparehand jobs it runs at once (default: suggested, see above). A 16 GB laptop: 1. A 32 GB desktop: 2. |
--cpus N | How many CPUs each job may use (default: suggested along with --jobs; all of them when --jobs is given). Test runners start one worker per CPU; on a 24-thread machine that can mean 23 processes of 1 GB each. With --cpus 8, Node tools (jest, vitest) see 8 CPUs, and Linux pins the job to 8 with taskset. |
--screen HOST:PORT | Its VNC / Screen Sharing address, for the app's Screens tab (screens.md). |
A rented machine (a pod at a GPU provider, paired like any other worker) gets its price with sparehand set NAME price 0.27 (USD an hour; - takes it away). sparehand list and sparehand status show it, and Studio never sends such a machine work by itself: you choose it on a storyboard (studio.md). A cloud worker made with sparehand cloud add (cloud-gpu.md) counts as paid the same way.
A worker on a slow or metered link (over Tailscale from elsewhere, a cloud machine) gets a limit for the copies to and from it with sparehand set NAME bandwidth 2000 (KB/s; - takes it away): the checkout's sync and what comes back (run --get, a session's changes) then go through rsync -z --bwlimit=2000, compressed. sparehand list shows it.
Then:
sparehand doctor worker1 # it answers, accepts the key, has the tools, has disk, a file goes there and back
sparehand status # load, memory, job slots of every machine
sparehand top Laptop # what is using that machine's memory and CPU
cd ~/code/my-project && sparehand run npm test
To change a worker later, edit ~/.config/sparehand/workers (one line each: NAME SSH-HOST MAX-JOBS CPUS SCREEN, - for "not set"), or sparehand remove it and add it again.
4. Harden the worker
A worker runs whatever the hub sends it, so make sure the hub is the only thing that can log in. On the worker, as root, create /etc/ssh/sshd_config.d/10-sparehand.conf (Linux and WSL; on macOS the same file works too, since macOS's sshd_config includes that folder):
# Keys only, no root, and only from the hub's address
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
AllowUsers alice@192.168.1.10
Add one alice@ADDRESS per address the hub may come from (for example its Thunderbolt and Wi-Fi addresses, or its Tailscale address). Before reloading, keep your current SSH session open and test the config:
sudo sshd -t && sudo systemctl reload ssh # Linux / WSL
sudo sshd -t # macOS: new connections use it right away
Then, from the hub, ssh -o BatchMode=yes worker1 true must still work, and a login from any other device (or with a password) must be refused.
More on what Sparehand trusts and why: security.md.
What you can do with a worker once it's added
- Run GPU jobs on a PC with an NVIDIA GPU.
- Reach its services (a model server, a database) from the hub.
- Open one of its folders in Finder.
Taking a worker out
sparehand pause worker1: no new jobs; a job running there is stopped and rerun elsewhere. The flag lives on the worker (~/.sparehand/paused), so the person using that machine can set it too.sparehand disable worker1: Sparehand stops contacting it at all. The flag lives on the hub, so the worker can't switch itself back on; use this when you don't trust a machine right now.sparehand remove worker1: forget it, after undoing everything Sparehand put there when it answers: file sharing, the AI model server, worker mode,~/.sparehandwith the run folders and caches, and the hub's key inauthorized_keys. Its own SSH server and tools stay (on Windows, the keep-alive task and the firewall rule need an administrator; it prints the two commands). It leaves all of that in place, and says so, when the machine doesn't answer, when another worker on this hub is the same machine, or when it runs jobs or keeps another hub's run folders.--keeponly forgets it;--cleancleans it even then (and refuses while it is off).