Appearance
Remote-URL Updates
Hive can fetch the daemon (hived), the CLI (hive), and the desktop / mobile app from an arbitrary HTTP(S) URL hosting a JSON manifest. This is independent of the desktop app's download-from-connected-daemon flow - it lets you stand up an update source behind any plain static HTTP server (S3, GitHub Pages, nginx, a raw folder served by python -m http.server for testing, etc.).
A daemon update is always a Hive node update: it requires the matching cli artifact for the node's platform, verifies and stages both binaries, then installs or updates the CLI when the daemon restarts. Hive does not expose a separate node CLI update control because the daemon and its local CLI are one release unit. If the staged CLI cannot be copied, made executable, or renamed into place on that restart, daemon startup fails instead of running a mixed daemon and CLI release.
During Clusto migration preparation, the legacy WebSocket UploadDaemon request is rejected even if its checksum is valid. It carries only a daemon and could strand a node with a mismatched CLI or without a verified A0 rollback image. Use a manifest URL through the app or hive update-node --from ...; the manifest flow stages the daemon and CLI atomically.
Manifest format
A manifest is a single JSON document. Every entry carries a SHA-256 digest that is verified after download - the manifest itself is not signed, so the authenticity of the bytes ultimately rests on the transport (TLS) and the hash check.
json
{
"version": "1.11.1",
"build_date": "2026-05-12T11:00:00Z",
"artifacts": [
{
"kind": "daemon",
"platform": "linux-x64",
"filename": "hived",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"size": 12345678,
"url": "hived",
"build_date": "2026-05-12T11:00:00Z"
},
{
"kind": "cli",
"platform": "linux-x64",
"filename": "hive",
"sha256": "abcd...",
"size": 9876543,
"url": "hive"
},
{
"kind": "app_installer",
"platform": "windows-x64",
"filename": "Hive-win-x64-1.11.1-setup.exe",
"sha256": "feed...",
"size": 87654321,
"url": "Hive-win-x64-1.11.1-setup.exe"
},
{
"kind": "android_apk",
"platform": "android-arm64",
"filename": "Hive-android-universal-1.11.1.apk",
"sha256": "cafe...",
"size": 30000000,
"url": "Hive-android-universal-1.11.1.apk"
}
]
}Notes:
kindis one ofdaemon,cli,app_installer,android_apk.platformslugs:windows-x64,linux-x64,linux-arm64,macos-x64,macos-arm64,android-arm64.urlis the location of the artifact bytes. Absolute URLs are used verbatim. Relative URLs are resolved against the manifest URL, so a folder layout likehttps://updates.example.com/hive/{manifest.json,hived,hive,...}works out of the box.build_dateis optional. When present, it is written next to the artifact as a<filename>.builddatesidecar - the daemon's artifact picker uses that to prefer fresh builds over stale siblings.sha256must be the lowercase hex digest of the artifact bytes. Mismatch aborts the update and removes the partial file.
Release channels (semi-stable / unstable)
Updates are published on two channels that share one accumulating folder of versioned binaries - only the manifest each channel points at differs:
| Channel | Manifest file | Who tracks it | When it moves |
|---|---|---|---|
| semi-stable (default) | manifest.json | the whole fleet, unless overridden | only on an explicit promote |
| unstable | manifest-unstable.json | machines opted in to newest builds | every deploy distribution |
manifest.json is also what the nginx index serves, so a base URL with no filename (https://hive.vazac.dev/) resolves to the semi-stable channel.
During the Clusto A0 transition, https://clusto.app/ serves the same update and install paths as https://hive.vazac.dev/, while https://dl.clusto.app/ serves the same immutable artifact store as https://dl.vazac.dev/. These are TLS aliases, not separate channels. The old Hive domains stay online for the whole transition, so nothing is stranded. Before publishing A0, verify both old/new pairs return the same stable and unstable manifests and that /install.sh succeeds through both web-origin names.
The Clusto hosts are now the default everywhere a new endpoint is chosen: install.sh (override with CLUSTO_BASE_URL, and HIVE_BASE_URL still works), a fresh daemon config, and the app's default manifest URL.
Nodes installed before the rename were carried across by 1.14.2, which rewrote a stored hive.vazac.dev / dl.vazac.dev base URL to its Clusto counterpart after verifying the new host served that node's channel manifest. That one-shot migration is gone from later releases: the Clusto hosts are the only default, and a node still pointing at an old host reaches the Clusto hosts by taking 1.14.2 first, which the old origin keeps serving.
Nodes and clients pick a channel rather than a raw URL:
- Node: the
[update].channelfield inconfig.tomldrives hived's own self-update and its auto-check broadcast. Edit it from Settings → Updates → Update source; it replicates to peers. - Client (app): the channel is stored as a global preference (
hive-update-channel) so it rides the cluster-preferences push/pull alongside every other setting. Edit it from Settings → Updates.
Publishing and promoting
bash
# Build, then publish the new version to the UNSTABLE channel:
pwsh ./scripts/clusto.ps1 build all
pwsh ./scripts/clusto.ps1 deploy distribution # writes manifest-unstable.json
# Once an unstable build is vetted, promote it to semi-stable:
pwsh ./scripts/clusto.ps1 promote # copies -> manifest.jsondeploy distribution always writes manifest-unstable.json (newest version per artifact) and, on the very first publish, seeds manifest.json from it so existing installs keep resolving. After that, manifest.json only changes when you run promote, which copies the current unstable manifest verbatim - the versioned artifacts it references already live in the folder, so no rebuild is needed.
App updates (Settings → Updates → App updates)
Settings has a standalone Updates tab. Its top card, App updates, drives this app's own self-update from the connected daemon and is built for unattended, interruption-tolerant downloads:
- Background, resumable download. Click Download (or enable auto-download) and the artifact streams to disk in the background. The bytes are saved to a
<filename>.partialsidecar as they arrive; if the transfer is interrupted (you leave, the socket drops, the app restarts), the next attempt sends an HTTPRangerequest and resumes from where it left off instead of starting over. The daemon's/clients/{kind}/{platform}endpoint advertisesAccept-Ranges: bytesand answers206 Partial Contentto make this possible. - Staged reuse - download once, install later. A completed, SHA-256 verified download is kept on disk. When the same update is rediscovered (on reconnect or relaunch) the app detects the staged file and jumps straight to Ready to install - so a cancelled or deferred install never re-downloads.
- Auto-download toggle. Off by default. When on, a newly discovered update is fetched in the background automatically so Install is instant. The preference is stored per device in
localStorage. - Install applies the staged artifact via the platform installer (NSIS on Windows, DMG on macOS,
.deb/.rpm/tarball/AppImage on Linux, system PackageInstaller on Android). See Linux desktop artifacts for how the Linux app picks which one it updates with.
The same staged + resumable download powers the auto Install now banner, so banner installs are equally interruption-tolerant.
Persistent settings (Settings → Updates → Update source)
The Update source card (below App updates in the same tab) persists the manifest URL, channel, optional bearer token, auto-check toggle, and interval across restarts. This card is the only place these fields are edited - the cluster config dialog no longer duplicates them.
- When connected to a daemon: Save writes to the daemon's
[update]section inconfig.tomland broadcasts to every cluster peer viaPeerMessage::SyncRemoteUpdateConfig. Each node ends up with the same URL/token in its local config, so a peer can still self-update on schedule even when other nodes are offline. - When not connected: Save stores the settings in browser
localStorageonly. The app keeps polling the URL locally so the desktop client can still receive an "update available" banner.
The token is propagated alongside the URL because cluster peers already share the cluster_token over the same authenticated peer transport - treat the manifest token with the same care.
Updating Hive from the desktop app (Settings → Daemon → Local Daemon)
The Local Daemon card on the Daemon tab exposes one Update Hive row. It pulls both hived and hive from the configured manifest URL and channel, using the same settings shown on the Updates tab:
- Update - updates the local daemon and matching CLI from the resolved manifest URL.
- When connected to the local node, it asks the daemon to self-update over the existing WebSocket. The daemon verifies and stages its own binary plus the same-release CLI, then restarts and installs the CLI beside the daemon. A missing or invalid CLI artifact fails the update before restart; a failure to install the staged CLI aborts the new daemon startup. A0 also verifies or creates its immutable rollback backup before allowing its process image to be replaced.
- When not connected, the app downloads and verifies both artifacts directly. It installs or updates the CLI first, then stops the daemon, replaces its binary, and restarts it. Replacing either binary in a system directory may require elevated privileges and surfaces a clear error when permission is denied.
The row compares the installed daemon version against the release manifest for the selected channel, not against anything shipped inside the app - the desktop app carries no daemon binary of its own. The manifest is fetched when the Settings page opens (and again after a channel switch), so the row reads either vX.Y.Z available, Up to date (manifest vX.Y.Z), or - when a locally installed build is ahead of the channel, e.g. an unstable build with semi-stable selected - Newer than the semi-stable manifest (vX.Y.Z). The button stays available in the up-to-date case, labelled Reinstall, so broken or partly written binaries can be replaced without waiting for a new release. When no manifest URL is configured, the row says so.
[update] section in config.toml
toml
[update]
manifest_url = "https://updates.example.com/hive/" # base directory URL
channel = "semi-stable" # or "unstable"
token = "optional-bearer-token"
auto_check = true
interval_secs = 21600 # 6 hoursmanifest_url is the base directory; the concrete manifest file is derived from channel (see Release channels). A base that already ends in a *.json filename has that segment replaced, so both https://host/ and https://host/manifest.json resolve correctly.
Missing section → defaults to auto_check = true, interval_secs = 21600, channel = "semi-stable", base URL https://clusto.app/.
Auto-check + manual install confirmation
When auto_check = true and manifest_url is set, hived spawns a background task at startup that:
- Sleeps for
interval_secs. - Fetches the manifest, compares its
versionagainstenv!("CARGO_PKG_VERSION"). - If newer, broadcasts
ServerMessage::RemoteUpdateAvailableto every connected client. The desktop app surfaces an "Install on this device / Update connected daemon" banner - the install itself is always manual: hived never auto-applies an update without explicit user confirmation.
Interval changes propagate on the next tick (no restart needed). Settings re-read every cycle, so cluster-replicated changes via SyncRemoteUpdateConfig take effect within interval_secs on every node.
Endpoints
All three targets accept an optional Bearer token via HIVE_UPDATE_TOKEN env var (CLI) or the dialog (desktop). When set, every HTTP request to the manifest URL and to each artifact gets Authorization: Bearer <token>. Public URLs work with no token.
CLI self-update from URL
bash
# Default channel (semi-stable) from the default base URL:
hive update
# Follow the unstable channel instead:
hive update --channel unstable
# Custom base URL, with a bearer token:
HIVE_UPDATE_TOKEN=secret hive update --from https://updates.example.com/hive/
# or
hive update --from https://... --remote-token secret--from is the base directory URL and --channel selects the manifest file. When either flag is omitted, the CLI reads the local hived config's [update] section and follows the same manifest_url and channel the node itself tracks (falling back to https://clusto.app/ and semi-stable when there is no config). --remote-token likewise falls back to the config's [update].token. So a plain hive update on an unstable-channel node pulls the unstable CLI with no flags. The CLI downloads the cli artifact for its own platform, verifies the SHA-256, replaces the running binary, and re-execs.
Daemon self-update from URL
The daemon doesn't poll on its own - it self-updates when asked, via the WebSocket. You can drive that from the CLI:
bash
hive update-node --from https://updates.example.com/hive/manifest.jsonA bare hive update-node updates the local node, following the [update] manifest URL and channel the node already tracks. Every run requires the same-platform daemon and CLI entries, verifies and stages both, and installs or updates the CLI as part of the daemon restart. If either required artifact is missing or fails verification, the node does not restart into a partial update. If the staged CLI cannot be installed beside the daemon on the next boot, daemon startup fails rather than accepting a mixed release.
Add --refresh-artifacts to additionally download the remaining entries: CLIs and daemons for other platforms plus app installers and Android APKs. These are written into the daemon's local clients/ and daemons/ trees so connected clients and peer relays can pull them later. The current node's CLI is always staged and installed even when this flag is omitted. Each peer daemon is brought up to date by running its own URL update against the same manifest.
The daemon side never auto-polls. Trigger an update explicitly - cron, systemd timer, a CI deploy step, or a human at the dialog.
Desktop app update from URL
Set the manifest URL, channel, and optional bearer token under Settings > Updates > Update source. Use App updates to update the current app, or Settings > Daemon > Local Daemon > Update Hive to update the local daemon and its matching CLI. The desktop app does not stage artifacts for other platforms; use hive update-node --refresh-artifacts when a daemon should cache the remaining manifest entries.
On desktop the installer is downloaded into the user's Downloads folder and run automatically (NSIS /S on Windows, open on macOS, pkexec apt install -y for .deb, pkexec rpm -Uvh for .rpm, pkexec tar -xzf … -C / or replace-and-restart for the tarball, replace-and-restart for AppImage). On Android the APK is staged into the app cache and handed to the system PackageInstaller via the JS bridge.
Linux desktop artifacts
Linux ships four desktop artifacts per release, all x64:
| Artifact | Manifest platform | WebKitGTK |
|---|---|---|
Hive-linux-x64-<ver>.deb | linux-x64-deb | system webkit2gtk-4.1 |
Hive-linux-x64-<ver>.rpm | linux-x64-rpm | system webkit2gtk-4.1 |
Hive-linux-x64-<ver>.tar.gz | linux-x64-tar | system webkit2gtk-4.1 |
Hive-linux-x64-<ver>.AppImage | linux-x64 | bundled (Ubuntu 24.04) |
Prefer the package or the tarball. The AppImage carries its own WebKitGTK plus that build's EGL/wayland libraries, and those crash against a much newer host Mesa: the WebKit process dies with EGL_BAD_PARAMETER and the window never paints (seen on RDNA4 + Mesa 26). The other three link against the system webkit2gtk-4.1, so they follow the host's graphics stack. The AppImage stays published only for distros with no system WebKitGTK 4.1.
The tarball is the .deb payload repacked (./usr/bin, ./usr/share), so a system-wide install is sudo tar -xzf Hive-linux-x64-<ver>.tar.gz -C /; it can also be unpacked into any user-owned prefix and run from there.
The running app reports which artifact it wants as its update platform: an AppImage (it sets APPIMAGE in its own environment) asks for linux-x64, otherwise the app picks linux-x64-deb when dpkg is present, linux-x64-rpm when rpm is, and linux-x64-tar as the fallback. An install therefore updates with the same artifact type it was installed from.
URL updates from the Cluster view
The Cluster view's toolbar exposes update actions once a manifest URL is configured under Settings → Updates → Update source:
Check Updates fetches the configured manifest and refreshes the per-node buttons so you can see which nodes are behind.
Update All hands the whole run to the connected daemon in a single message. The daemon fetches the manifest, picks every node whose running daemon version is older than it, updates the peers, and self-updates last. Each node stages its matching daemon and CLI before it restarts. It reports "All nodes already on vX.Y.Z or newer" when nothing is behind, and lists how many offline nodes it skipped.
The run lives on the daemon, not in the app: you can close the client right after clicking and the cluster still finishes the update. Progress arrives as one acknowledgement per node, broadcast to every connected client, so a second app (or a reconnecting one) sees the results too. The same is true of a single node's Update from URL - the download and staging survive a disconnect.
Each node card also shows an Update from URL (vX.Y.Z) button - the connected node and each peer - once a manifest version newer than that node is known. The manifest version is populated by Check Updates, the Check now button under Settings → Updates → Update source, the daemon's auto-check broadcast, or the client-side poll.
Node cards still report the installed CLI version, but there is no separate install or update button. Update from URL updates the daemon and installs or updates the matching CLI beside hived in the same operation.
Triggering an update asks the connected daemon to drive it:
- Connected node: the daemon runs the same flow as the
update-node --fromCLI path locally. - Peer: the connected daemon forwards a
SelfUpdateFromUrlover the inter-node WebSocket. The peer fetches the manifest itself, picks the daemon and CLI artifacts for its own platform, verifies both, and restarts - the manifest URL/token travel over the authenticated peer transport, the binary bytes never do. A manifest missing either artifact for the peer's platform comes back as a failed update acknowledgement.
Because every node downloads from the manifest URL directly, no node needs to host binaries for another node's platform.
Both paths run in a detached daemon task. The client's message only starts the work; the acknowledgement comes back over a broadcast channel when the node actually finishes, which is why closing the app mid-update no longer cancels it. Peers are updated concurrently, and the connected node updates last so it stays alive to dispatch and report on the others before its own restart.
Producing a manifest
A simple shell helper to generate manifest.json from a dist/ folder:
bash
sha() { sha256sum "$1" | cut -d' ' -f1; }
sz() { stat -c%s "$1"; }
cat > manifest.json <<EOF
{
"version": "$VERSION",
"build_date": "$(date -u +%FT%TZ)",
"artifacts": [
{"kind":"daemon","platform":"linux-x64","filename":"hived","sha256":"$(sha hived)","size":$(sz hived),"url":"hived"},
{"kind":"cli","platform":"linux-x64","filename":"hive","sha256":"$(sha hive)","size":$(sz hive),"url":"hive"}
]
}
EOFIn practice you don't hand-write this: pwsh ./scripts/clusto.ps1 deploy distribution stages the versioned binaries and generates the channel manifests for you (unstable on every run, semi-stable on promote). See Release channels. The snippet above is only useful for a one-off static endpoint.
Security model
- TLS is the only end-to-end integrity check on the manifest: there is no manifest signature. Pick
https://URLs from a host you control, or put the static folder behind an authenticated endpoint and use the bearer token. - SHA-256 is verified on every artifact after download; mismatch aborts the install and deletes the staged file. Tampering with bytes alone does not produce a working install.
- No replay protection: a stale manifest still installs cleanly as long as the SHA-256 matches. If you publish a vulnerable older version under the same manifest URL and don't tighten access, clients pointing at that URL will install it. Treat the manifest URL like any other deployment surface.
Pre-existing alternatives
The remote-URL flow doesn't replace the existing update paths - it sits beside them:
hive update(no--from): pull from the daemon you're connected to.- Cluster view → "Update peer from this node": daemon-to-daemon relay over the inter-node WebSocket. No HTTP round-trip; bytes never leave the cluster. The receiving daemon writes the synced artifacts into its local
clients/anddaemons/trees next tohived; on Windows installs (C:\Program Files\Hive) the install step pre-creates those dirs and grantsBUILTIN\UsersModify so the relay succeeds even whenhivedis not running as the elevated LocalSystem service. A relay failing withAccess is denied. (os error 5)means the target was installed before this provisioning existed - re-run the node install to repair the ACLs. - Auto-banner in the desktop app: triggered when the connected daemon's bundled artifact is newer than the running build.
Use the remote-URL flow when there is no upstream daemon to pull from, or when you want one URL to be the source of truth across an unmanaged fleet.