Install, run as a service, upgrade, uninstall
Release artifacts (built by packaging/release, see Building releases):
| File | Contents |
|---|---|
merkhain_<ver>_<os>_<arch>.tar.gz (linux, darwin) / .zip (windows) |
merkhain binary, README.md, share/ service files |
merkhain_<ver>-1_<arch>.deb, merkhain-<ver>-1.<arch>.rpm |
/usr/bin/merkhain, systemd units (nothing is enabled) |
merkhain.rb |
Homebrew formula for a tap of your own |
SHA256SUMS |
SHA-256 of every artifact |
Architectures: amd64 and arm64 for macOS, Linux and Windows. The binary is static (CGO_ENABLED=0); it needs no
runtime, SQLite or PostgreSQL client library, or root. It stores data in SQLite files by default; PostgreSQL is
optional (Databases).
Install
Verify first: sha256sum -c SHA256SUMS --ignore-missing (macOS: shasum -a 256 -c SHA256SUMS --ignore-missing).
macOS / Linux archive
tar -xzf merkhain_<ver>_<os>_<arch>.tar.gz
install -m 0755 merkhain_<ver>_<os>_<arch>/merkhain ~/.local/bin/merkhain # any directory on PATH
xattr -d com.apple.quarantine ~/.local/bin/merkhain 2>/dev/null || true # macOS, downloaded via a browser
merkhain version
Debian / Ubuntu / Fedora: sudo dpkg -i merkhain_<ver>-1_amd64.deb or sudo rpm -i merkhain-<ver>-1.x86_64.rpm.
Homebrew: put the rendered merkhain.rb into a tap (brew tap-new you/merkhain, copy it to
$(brew --repository you/merkhain)/Formula/), then brew install you/merkhain/merkhain.
Windows: extract the zip (e.g. to %LOCALAPPDATA%\Programs\merkhain), add that directory to your user PATH,
open a new terminal, run merkhain version.
Then continue with Local memory (identity, provider, hooks, MCP).
Service
serve local is a per-user, loopback-only process. Capture hooks and merkhain mcp work without it, but extraction,
transcript reconciliation, and sync only run while it does. Stop it with SIGTERM/Ctrl-C; in-flight jobs are reclaimed
on the next start.
Create your identity (merkhain identity init) before starting the service: a service started without an
identity keeps sync disabled until it is restarted.
Linux (systemd user unit) — the .deb/.rpm ships it at /usr/lib/systemd/user/merkhain-local.service; for an
archive install copy share/systemd/merkhain-local.service to ~/.config/systemd/user/ and fix ExecStart:
systemctl --user daemon-reload
systemctl --user enable --now merkhain-local
loginctl enable-linger "$USER" # optional: keep running while logged out
journalctl --user -u merkhain-local -f
With the file key store, uncomment MERKHAIN_KEY_PASSPHRASE_FILE in the unit (systemctl --user edit merkhain-local) and point it at an owner-only (chmod 600) file.
macOS (launchd) — Homebrew: brew services start merkhain. Otherwise:
sed -e "s|@MERKHAIN_BIN@|$(command -v merkhain)|" -e "s|@HOME@|$HOME|" \
share/launchd/ai.merkhain.local.plist > ~/Library/LaunchAgents/ai.merkhain.local.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.merkhain.local.plist
tail -f ~/Library/Logs/merkhain-local.log
Windows (logon task, no admin rights):
powershell -ExecutionPolicy Bypass -File share\windows\install-task.ps1
Services run with a minimal PATH. If you use the codexcli or claudecli provider, make sure codex or claude is
on the unit's/plist's PATH (both files set one), or set provider.codexcli.binary / provider.claudecli.binary to an
absolute path.
Upgrade
Upgrade the binary the usual way (replace it, dpkg -i / rpm -U / brew upgrade). There is no migration command to
run and nothing to stop first: the next time merkhain opens the local database (any command, a capture hook, the MCP
server or serve local) it applies pending schema migrations by itself, on SQLite and PostgreSQL alike. A database
older than the migration baselines upgrades with one earlier binary first. The Personal sync relay database
(personal-sync-relay) upgrades the same way when serve local next starts it.
- A migration runs in one transaction under the database's migration lock. Processes that open the database at the same
moment wait briefly for it (a capture hook at most its own time budget, after which it reports a retryable failure,
exit 75, and the next hook retries); nobody needs to be stopped. A migration that fails rolls back completely and the
next normal open tries again.
merkhain migrate statusshows what is pending and changes nothing;merkhain migrate up --localapplies it right away if you prefer. - Processes of the previous version that are still running (the service, an MCP server an agent started) keep working
against the upgraded database, and restart against it: from the first release with automatic migrations (the
bridge release) on, every schema change keeps older binaries' reads and writes working.
migrate statusfrom an older binary reports such a database asahead, which is healthy. Restart the service when convenient (systemctl --user restart merkhain-local,launchctl kickstart -k gui/$(id -u)/ai.merkhain.local,brew services restart merkhain, or stop and start the scheduled task) so it runs the new code. - Rolling back means running the older binary against the upgraded database; it never reverts the schema
(
migrate downandmigrate forcerefuse a schema newer than the binary). Back up before major upgrades anyway (see Operations; PostgreSQL: Databases) so you can return to the exact earlier state. - Transition: binaries released before the bridge release keep their old rules. They refuse a database a newer
binary migrated (
schema too new) and only migrate withmerkhain migrate up --local. Upgrade every computer and process to the bridge release first; from then on upgrades need no coordination.
If the binary path changed, re-run merkhain claude install and merkhain codex install (both update their hook
entries in place; Codex asks you to trust the changed hooks again). Re-running them after any upgrade also updates the
merkhain agent skill to match the new CLI. merkhain status should report no errors.
Two checks can stop a first upgrade from an older release. Neither has an opt-out:
- A database an older release created under your umask, usually mode 0644, is refused by read-only commands (
status --direct,migrate status). Runchmod 600on it and its-wal,-shmand-journalfiles, which changes the mode and nothing else, or a command that opens it for writing, which does that and may apply pending migrations, so back it up first: any normal command for an instance on SQLite,merkhain migrate up --relayfor a relay.migrate statusprints the command for each refused database, such as the personal sync relay's own file, and these go file by file: each is for that file's owner, as nothing read-only repairs one.serve remotealso opens for writing, so it makes the file private, then still asks formigrate upif the schema needs it. - Running as root is refused unless every directory and link on the database's path is root's and writable only by
root. Run as the database's owner (
sudo -u <owner>, orUser=in the service) or move the database to such a path (Security).
Standalone relay servers (serve remote) migrate explicitly: back up the relay databases, run
merkhain migrate up --relay with the new binary while the old relay processes keep serving, then roll out the
new binary to the relay processes one at a time. Old relay processes keep working against the migrated databases, so
rolling the application back is starting the old binary again. Keys, identities and pool memberships are unaffected by
upgrades. Details, including the systemd and container commands, are in
Databases. packaging/ci/upgrade-smoke.sh OLD NEW checks the upgrade from
a pre-#69 binary and packaging/ci/mixed-version-smoke.sh OLD NEW the mixed-version rollout (CI builds the binaries
from source).
Behavior change for relay operators upgrading past the explicit migrations (#69): resending a key upload, offer or
approval for a recipient who was removed from the pool meanwhile now returns permission_denied instead of a no-op
success (stored: 0).
Uninstall
Removal keeps your data unless you delete it. Export recovery material first if you might come back (Operations).
merkhain claude uninstall # removes only merkhain's hook entries and the merkhain skill; other settings are kept
merkhain codex uninstall # removes only merkhain groups from ~/.codex/hooks.json and ~/.agents/skills/merkhain
claude mcp remove --scope user merkhain
# delete the [mcp_servers.merkhain] table from ~/.codex/config.toml
systemctl --user disable --now merkhain-local # or launchctl bootout / install-task.ps1 -Uninstall
Then remove the binary (rm, dpkg -r merkhain, rpm -e merkhain, brew uninstall merkhain). To delete all
data too: remove the directories printed by merkhain config (config_dir, data_dir) and, for the native key
store, the keychain/credential entries of service merkhain (macOS: security delete-generic-password -s merkhain,
repeat until none is left). Deleting them on your only device destroys every private memory and your pool keys.
Building releases
make release VERSION=v1.0.0 # = go run ./packaging/release -version v1.0.0
make smoke VERSION=v1.0.0 # clean-machine smoke of the host archive (packaging/smoke.sh)
make container-smoke # relay image + TLS startup check (packaging/container/)
packaging/ci/postgres-smoke.sh ARCHIVE [IMAGE] # the smoke and the container on PostgreSQL (needs docker)
packaging/ci/upgrade-smoke.sh OLD_BINARY NEW_BINARY # upgrade from an older release keeps the data
VERSIONand-commitmay contain only letters, digits,.,+and-(SemVer orgit describeoutput, at most 128 characters, starting with a letter or digit). Bothmakeandpackaging/releaserefuse anything else, so a version never reaches a shell, the linker flags or the Ruby formula as code.- Builds are reproducible:
-trimpath,-buildvcs=false, empty build ID, version/commit via-ldflags -X, archive timestamps fromSOURCE_DATE_EPOCH(default: the commit time), sorted entries with root ownership. Two builds of the same commit produce identicalSHA256SUMS. - The web UI is embedded from the checked-in
internal/webui/dist, so release builds need no Node.js.make web-checkrebuilds it fromweb/(Node 22.13+) and fails if the result differs; see Web UI.packaging/smoke.shchecks the UI routes, assets and launch-link sign-in, and the relay console's operator login.packaging/container/smoke.shchecks the console and itsSecurecookie over native TLS. .deb/.rpmneed nfpm onPATH(go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0); without it they are skipped (-packages onmakes that an error).- Archives carry the
-versionstring as is..deb/.rpmneed a version dpkg/rpm accept: SemVer versions pass through; anything else (e.g.git describeon an untagged commit) becomes0.0.0~git<commit date>.g<commit>. -targets linux/amd64,...limits the matrix;-base-urlsets the download URL in the Homebrew formula.- CI (
.github/workflows/release.yml, onv*tags, manual dispatch, and packaging/web UI PRs) builds everything twice, compares checksums, installs the .deb, builds and smoke-tests the container, and runspackaging/smoke.shon native Linux (amd64, arm64), macOS (arm64, amd64) and Windows runners. It also starts the shipped service definitions (systemd user + relay units from the .deb, the launchd plist, the Windows logon task) and installs the formula from a local Homebrew tap (packaging/ci/). Artifacts are kept on the workflow run; nothing is published.