What hand update does
To update an installed Hand, run hand update from anywhere. It does the following:
- downloads the newest build of the channel the running
handcame from, and checks it; - backs up every fleet's
hand.dbtobackups/in the fleet folder, and the old binary to~/.secondhand/backups/; - replaces the binary, then stops each fleet's watcher and the board by the process IDs they record (
watch.pidin the fleet folder,board.pidin~/.secondhand) and starts them again ashand supervisor startandhand opendo, on every OS (the board first, once it has writtenboard.generation, then the watchers, so a watcher holds the new board's generation and the board's sweep does not restart it); - runs
hand initin every fleet, which writes the fleet'shand.versionpin (see Fleets). When the update would replace the binary, it first reads every fleet's pin (so doeshand update --check) and refuses, before any backup or swap, when the build it would install is older than a pin, naming every fleet that refuses: a downgrade from edge to stable would leave those fleets refused. The hint ishand update --channel edge, or, when the target is already edge, that no release of the channel is that new yet. A build that is already current installs nothing, so it is never refused for a pin. To go back on purpose, deletehand.versionand restore a backup only if the older hand refuses the schema, as in Restore a backup;
The first hand update or fleet command of a version that keeps a fleet's own luvus/<os>-<arch>/, backups/ and cache/models/ copies the old machine-side files into the fleet and prints a migrated list. It copies the backups once (a backups/.migrated file marks it done, so a backup that Hand pruned does not come back), it never moves or removes: a Luvus server that runs from ~/.secondhand/luvus/ keeps running from that binary, because the fleet's pin names the same version and hand update switches a server only when the version differs. hand doctor lists the old copies under old machine copies; delete them yourself once no Luvus server runs from the machine copy and you no longer need the older restore points.
It also fetches the hand-iroh sidecar of that release, when the new hand names one (the iroh: line of hand version) and the release carries it, and puts it next to hand: see The hand-iroh sidecar. The asset and its .sha256 are checked, and the sidecar must report the version the new hand needs; if any check fails, nothing is replaced and the update stops before it touches hand, a fleet or a process. A release without the asset (0.13.0 and earlier, and edge) is not an error: the report says iroh: none in this release and an installed sidecar stays. The sidecar is placed before hand is, so a failure to place it leaves the old hand in place, and the board's restart at the end starts the new sidecar with the new board. If placing hand itself then fails, the old hand stays with the new sidecar: hand doctor warns about the mismatch, and the next hand update replaces both. An update that finds hand already current does not replace the hand, but it still installs the sidecar when it is missing or is not the version that hand needs, and the release carries that version: this is how an update run by 0.13.0, which installs the new hand without a sidecar, is repaired by the new hand's next update. hand update --check reports iroh: would install for that, or iroh: none in this release when the release carries no sidecar, and changes nothing. On Windows the running sidecar is renamed to hand-iroh.exe.old as hand.exe is, and the next hand run removes it.
It also fetches hand-desktop, the native window, on Linux amd64 and arm64, and puts it next to hand. The release names the asset hand-desktop-linux-amd64.tar.gz (or hand-desktop-linux-arm64.tar.gz), with a .sha256 beside it; both are checked, and the binary must print the version and commit that the new hand reports in hand version (hand-desktop VERSION COMMIT) before anything is replaced. A release without the asset is not an error: the report says desktop: none in this release and an installed window stays. On macOS, Windows and any other architecture the report says desktop: unsupported on OS/ARCH and nothing is downloaded; the Board works in a browser there. On Linux amd64 and arm64 an update that is not --check also keeps the launcher entry in step with the window next to hand: it writes $XDG_DATA_HOME/applications/hand-desktop.desktop (default ~/.local/share/applications; Name=Hand, Exec= and TryExec= the absolute path of the installed hand-desktop, Icon=hand-desktop, X-Hand-Managed=true, Terminal=false, Categories=Development;, StartupWMClass=hand-desktop) and the icon $XDG_DATA_HOME/icons/hicolor/512x512/apps/hand-desktop.png, each atomically and only when its content differs, so a moved install is rewritten and a repeat run changes nothing. With no window installed (no asset in the release) it removes both files only when the entry carries the X-Hand-Managed=true line it writes; an entry without it is left alone and reported as a help line. It then runs update-desktop-database if that exists and ignores its failure; a failed write is a help line, not a failure of the update. The window is placed before hand is, as the sidecar is, so a failure leaves the old hand and the old window in place. An update that finds hand current still installs a window that is missing or does not carry that version and commit; when that download fails a check, the report says desktop: failed: <reason>, the window stays as it was, and the switch of the stale fleets still runs. hand update --check reports desktop: would install, desktop: none in this release or desktop: unsupported on OS/ARCH, and changes nothing.
It also pins the Luvus version that build was tested with when that is newer than the pin. On every OS it compares the version a UHP ping reports with the pin's: a server that does not answer is not running and has nothing to switch, and one that answers without a version is unknown and is never switched.
A server that runs another version than the pin is switched, fleet by fleet, only when the fleet is quiet and settled. Quiet: no worker attempt is live, the supervisor is idle, no message waits for it, and no pane is live in that server besides the supervisor's and the shell Luvus opens when it starts. Settled: the fleet recorded no event for 15 minutes. The checks run in this order, and a fleet that fails one stays pending: <reason> in the fleets rows, which names a pane Hand did not start, since stopping the server ends it (a check before step 2 changes nothing, and the watcher keeps running):
- quiet and settled;
- stop that fleet's watcher, so nothing wakes the supervisor;
- the supervisor's screen revision (
content_revision) is unchanged over 30 seconds, and the fleet is still quiet; hand supervisor stop, then check quiet again: a message queued meanwhile resumes the supervisor and leaves the fleet pending;- stop the server (it sends
server.stopand waits up to 10 seconds for the process in the session folder'sserver.pidto exit; it ends the process itself, its whole process tree on Windows, only when the stop was acknowledged or the server no longer answers, so a server that answerspingbut refusesserver.stopis left running, and a recycled process ID is never signalled), start the pinned copy, and require thatpingreports the pin's version; - resume the supervisor, start the watcher, and record the event
luvus.switched(0.14.3 -> 0.15.0), whichhand orientlists and the board shows as one line in the chat.
Any failure at step 4 to 6 (the supervisor's stop, the server's stop or start, a wrong version) records luvus.failed with the reason (a server that comes back with another version is one), puts the supervisor and the watcher back, and leaves hand update with a non-zero exit. The board's timer does not try that pin again; your own hand update does, once the fleet is settled again. luvus.failed and luvus.switched are events like the others, so the 15 minutes start again after each. A hand update run inside a fleet's Luvus panes (LUVUS_SESSION names that fleet's session) leaves that fleet pending; the board's timer runs outside every pane and may switch it later.
What a switch costs:
- a
hand attachviewer is disconnected (Luvus cannot tell Hand who is attached): attach again; - text typed but not sent in the supervisor's pane is lost if the screen stayed still for the 30 seconds (a supervisor whose screen keeps changing while idle never passes that check, so its fleet stays pending);
- no worker loses work, because a switch needs no live attempt and no pane that Hand did not start; the files in the worktrees stay;
- the supervisor comes back as a new
sNon the same session.
When the binary is already current and no newer Luvus needs pinning, hand update visits only the fleets whose server is stale: it does not stop the board, does not run hand init, and leaves every other fleet's watcher alone. hand update --check lists a fleet's server as not running or unknown under that name, plans no switch for it, and shows would switch or would stay pending: <reason> for a stale one. hand update signals only a process whose recorded binary is the one being updated, and only while its recorded start time still matches, and it restarts only what it stopped. It stops a process with SIGTERM (TerminateProcess on Windows), which exits cleanly, so systemd does not restart an installed unit itself: an installed board unit is started again, and an installed watch unit stays inactive while the watcher runs as the transient unit secondhand-watch-<fleet id> until your next login. If both ever run, the second watcher finds watch.lock held and exits with status 3, and a second board fails to bind the board's address. While an update runs, the board starts no watcher. Once it starts changing things it ignores Ctrl-C, and its own steps run outside the terminal's process group, so it does not leave a fleet without its watcher. If it is killed anyway, the next hand update finishes the steps it left, from its journal at ~/.secondhand/update.json. A supervisor that could not be resumed after a switch stays in that journal, so the next hand update, including the board's timer, resumes it first. A fleet whose pin.json is unusable (unreadable, an absolute path, or a copy that resolves outside the fleet's luvus/ folder) or whose pin cannot be written is reported as failed and left as it is; the other fleets go on, and the update exits non-zero. A failed layout migration prints its error once a day on a plain command (hand init and hand update try again every time). A failed update can leave the new Luvus pin in place; a running server ignores it, and the next hand update carries on. A failed switch never edits pin.json itself: the pin moves before any fleet is visited, and hand luvus pin BIN changes it by hand.
Restore a backup
To restore a backup:
- stop the board, then the fleet's watcher, so the board cannot start it again: run
hand board stop, thenhand watch stopin the fleet (or with--home DIR). Each signals a process only when its recorded start time, binary and boot id still match, ashand updatedoes, and reportsstopped, ornot runningwhen none runs or the pid file was stale (it removes that file). If a process is another binary's, it exits 3 and leaves it running; if it is still alive 10 seconds after SIGTERM, it exits 3 too. The rest of this step is the fallback for a machine wherehanditself is not usable: their process IDs startboard.pidin~/.secondhandandwatch.pidin the fleet folder, in one linePID MARKER PATH, whereMARKERis the process's start time as Hand read it. A second line,boot ID HOST, appears on Linux and macOS: if its boot id is not this machine's current one (/proc/sys/kernel/random/boot_id,sysctl -n kern.bootsessionuuid), the process ID belongs to another host or an earlier boot, so leave it alone. An olderhandignores that line and, because it then reads another binary path, leaves the process running. A stale file can name an ID that another program now holds, so prove the process is the one that wrote the file before you stop it, and leave a file that fails the check alone. Run the snippet for your OS, then again withwatch.pid:- Linux: the start time must equal
MARKER, which is what Hand checks:same() { [ "$(sed 's/.*) //' /proc/$1/stat 2>/dev/null | cut -d' ' -f20)" = "$2" ]; } read pid marker _ < ~/.secondhand/board.pid same $pid $marker && kill $pid while same $pid $marker; do sleep 1; done - macOS: the start time in whole seconds must equal
MARKERbefore its dot:same() { [ "$(date -j -f '%a %b %e %T %Y' "$(ps -o lstart= -p $1 2>/dev/null)" +%s 2>/dev/null)" = "${2%%.*}" ]; } read pid marker _ < ~/.secondhand/board.pid same $pid $marker && kill $pid while same $pid $marker; do sleep 1; done - Windows: the start time as a file time must be within one second (10,000,000 ticks) of
MARKERafterwindows::$id, $marker, $null = (Get-Content ~\.secondhand\board.pid).Split(' ') if ([math]::Abs((Get-Process -Id $id).StartTime.ToFileTime() - [int64]$marker.Replace('windows:', '')) -lt 1e7) { Stop-Process -Id $id; Wait-Process -Id $id -ErrorAction SilentlyContinue }
- Linux: the start time must equal
- run
hand supervisor stopin the fleet if its supervisor runs, at a time when no worker runs, so nothing writes tohand.db; - remove
hand.db-walandhand.db-shmnext to the fleet'shand.db; - copy the backup over
hand.db: ahand.db.<stamp>fromhand update, or a dailyhand-YYYYMMDD.dbfrom the watcher, both inbackups/in the fleet folder. Backups made before the fleet kept its own (an olderhand, or the daily snapshot of a watcher that has not restarted yet) are in$SECONDHAND_HOME/backups/<fleet id>/(~/.secondhand/backups/<fleet id>/unless you set it): Hand copies them into the fleet once and leaves the originals, andhand doctorprints the newest restore point of either place, newest by the stamp in the file name, in itsrestore pointsrow. If thehandyou go back to is older than the one that last opened the fleet, also deletehand.versionin the fleet folder (with the supervisor and board stopped, as in steps 1 and 2): that olderhandrefuses the fleet while the pin names a newer one, no backup holds the pin, and it writes a new pin at its next open. When both read the same schema, deletinghand.versionis enough and thehand.dbrestore can be skipped, which keeps what the fleet recorded since the backup; - put the old binary back from
~/.secondhand/backups/hand.<stamp>if the new one is the problem (Hand backs uphand, not the sidecar: an olderhandthat pins another sidecar version showshand-irohas failed inhand doctoruntil you install the matching one); an olderhandrefuses a database that a newer one migrated (newer than this hand), so step 4 is not optional with it; - run
hand opento start the board again, then start or resume the supervisor from the fleet's page.
Updating from before 0.13.0
This build migrates hand.db only from schema 16, the one Hand 0.13.0 writes. It refuses an older database before it migrates anything, with PATH has schema N, older than this hand reads (16 to 23); hand 0.13.0 is the last release that migrates it, where PATH is the database file (a migration that finds the old schema by itself says state schema N, older ... instead). This is a break, with no switch to go back. From an older release, install 0.13.0 first (HAND_INSTALL_VERSION=v0.13.0, see Install), run hand orient in each fleet so it migrates the database (hand version does not open it), then update. A hand update run by an older release swaps the binary before anything reads the database, so run the 0.13.0 step first. If one already swapped it, every hand command that opens the database now refuses it: install 0.13.0 over it with HAND_INSTALL_VERSION=v0.13.0, run hand orient in each fleet, then run hand update again.
Updating from 0.13.0
hand update runs the code of the build you have until it has swapped the binary, and restarts the watcher and the board with that code. From 0.13.0, with both a watcher and the board running, the new watcher migrates hand.db first, and 0.13.0 then refuses to open the migrated database to start the board. The update still replaces the binary, backs everything up and refreshes the fleet, restarts the watcher, and then exits 1 with the unit row board,start,"failed: invalid: state schema N is newer than this hand (16)" and the help line could not start board. Nothing is lost: run hand open to start the board on the new binary. With only the board running, or with neither, the update succeeds without that line.
Workers start without MCP servers
A Claude worker no longer loads the MCP servers of your own Claude config or of a repository's .mcp.json: it starts with --strict-mcp-config and none. This is a break, with no switch to go back. If your workers rely on a browser or another MCP server, put the servers in mcp.json in the fleet home and start those workers with hand attempt start --mcp mcp.json (see MCP); hand attempt start refuses a file that is missing.
Routing profiles are gone
routing.json no longer has profiles. A routing.json that still holds the key does not parse: hand doctor fails its routing.json row with "profiles" was removed; delete the key and start workers with --harness, --model and --effort, and the watcher reads nothing from the file. hand attempt start, hand supervisor start and hand supervisor switch take --harness, --model and --effort, and --profile is an unknown flag. hand route list is gone: hand models lists models and hand attempt track prints the track record. access: readonly, approve: ask, approve_timeout, hand project grants and the attempt.denied wake went with them.
There is a window between hand update and the edit. Until you delete the key, the watcher falls back to the desktop notifier (the notify command does not run, so phone alerts move to the desktop and are not lost), and autoresume, inbox pruning, worktree retention and stall delays use their defaults or stay off. The Board's watcher sweep does not restart a stopped fleet's watcher while profiles is present: it logs one stderr line per fleet, and the line stops when you delete the key. hand orient and the Board are unaffected. Do this in every fleet, in the same sitting:
- Before
hand update, carry each profile you want to keep intomemory/operator.mdas standing rules in flag form, for example "workers run--harness claude --model opus --effort high". The supervisor's skill falls back to--harness claude --model sonnetwhen the file says nothing, and its levels arelow,medium,high,xhighandmax(the oldquick,default,high,deepandmax). - Run
hand update, then delete only theprofileskey from each fleet'srouting.json. The file keepsnotifyand any other key;hand initwrites{}for a new fleet. - Run
hand doctorin each fleet, and rewrite the profile mentions in the fleet's memory.
Options
The options:
hand update --checkshows what would change and changes nothing; itsunitsrows list the stop and start steps a real run would take for each fleet's watcher and the board, each markedwould run;hand update --channel edge(orstable) moves to the other channel;hand update --keep-luvusnever switches a fleet's Luvus server: a version difference stayspending: --keep-luvus, and a current binary returns at once;hand update --autois what the board's timer passes (Update on a timer): it does not try a switch again for a pin that already failed;- a
handbuilt from source needs--channel.
Update on a timer
hand board --update-every 1h makes the running board start hand update every hour, detached, on every OS. It is off by default (0), and the least it accepts is 10m. The automatic run uses the channel the installed binary was built for, so a build from source cannot use it: the board says so once and starts without it. The timer is what makes Hand switch a fleet's Luvus server by itself: each run switches the stale fleets that are quiet and settled (see What hand update does), and with a current binary it visits only those. Turn the timer off (0), or run hand update --keep-luvus by hand, to never switch. A second hand update while one runs exits with a message, and the board skips its turn. The last automatic run's output is in update.log in ~/.secondhand. When the update restarts the board, the new board keeps the interval, which the board records in board.update-every (the last board start wins: one started without the flag clears it). A turn skipped because an update runs or the lock is held is tried again within 30 seconds. On Linux, add the flag to ExecStart= in ~/.config/systemd/user/secondhand-board.service and add KillMode=process under [Service], so that systemd does not end the update when the board stops, then run systemctl --user daemon-reload and systemctl --user restart secondhand-board:
ExecStart="/home/you/.local/bin/hand" board --addr 127.0.0.1:7777 --update-every 1h
KillMode=processOn macOS add <string>--update-every</string><string>1h</string> after <string>board</string> in the plist, and on Windows add --update-every 1h to the shortcut's Arguments.