merkhain

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 status shows what is pending and changes nothing; merkhain migrate up --local applies 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 status from an older binary reports such a database as ahead, 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 down and migrate force refuse 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 with merkhain 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). Run chmod 600 on it and its -wal, -shm and -journal files, 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 --relay for a relay. migrate status prints 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 remote also opens for writing, so it makes the file private, then still asks for migrate up if 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>, or User= 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
  • VERSION and -commit may contain only letters, digits, ., + and - (SemVer or git describe output, at most 128 characters, starting with a letter or digit). Both make and packaging/release refuse 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 from SOURCE_DATE_EPOCH (default: the commit time), sorted entries with root ownership. Two builds of the same commit produce identical SHA256SUMS.
  • The web UI is embedded from the checked-in internal/webui/dist, so release builds need no Node.js. make web-check rebuilds it from web/ (Node 22.13+) and fails if the result differs; see Web UI. packaging/smoke.sh checks the UI routes, assets and launch-link sign-in, and the relay console's operator login. packaging/container/smoke.sh checks the console and its Secure cookie over native TLS.
  • .deb/.rpm need nfpm on PATH (go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0); without it they are skipped (-packages on makes that an error).
  • Archives carry the -version string as is. .deb/.rpm need a version dpkg/rpm accept: SemVer versions pass through; anything else (e.g. git describe on an untagged commit) becomes 0.0.0~git<commit date>.g<commit>.
  • -targets linux/amd64,... limits the matrix; -base-url sets the download URL in the Homebrew formula.
  • CI (.github/workflows/release.yml, on v* tags, manual dispatch, and packaging/web UI PRs) builds everything twice, compares checksums, installs the .deb, builds and smoke-tests the container, and runs packaging/smoke.sh on 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.