Perseus Vault upgrade and migration playbook
August 3, 2026 · View on GitHub
This playbook upgrades a local source-built installation without changing the location of its durable database. It is deliberately explicit about the boundary between the binary, the client configuration, and the database. It does not promise a generic automatic database migration: do not invent a migration command or copy a newly initialized empty database over an existing one.
The normal upgrade path below keeps the same database and passes its absolute
path to every command. The existing migrate --from ... --to ... command is a
separate, format-specific path for a legacy v0.1.x source database and an
explicit target; it is not a generic command for every binary upgrade.
1. Preflight and backup
Stop or disconnect every MCP client and server process that can write the store before copying it. A live SQLite/WAL copy is not a restore proof. Choose one absolute path and use it consistently:
set -euo pipefail
SRC="/path/to/perseus-vault"
PROJECT="/path/to/agent-project"
DB="/absolute/path/to/perseus-vault.db"
BIN="$HOME/.local/bin/perseus-vault"
STAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="$HOME/.perseus-vault/backups/$STAMP"
mkdir -p "$BACKUP_DIR"
# Refuse to update a checkout with unreviewed local edits.
test -z "$(git -C "$SRC" status --porcelain)"
git -C "$SRC" rev-parse HEAD > "$BACKUP_DIR/source-before.txt"
"$BIN" --version > "$BACKUP_DIR/binary-before.txt"
# Copy the database only after all writers have stopped. Keep SQLite companions
# when present so the backup set is self-contained.
for suffix in "" "-wal" "-shm"; do
source="${DB}${suffix}"
if [ -e "$source" ]; then
cp "$source" "$BACKUP_DIR/$(basename "$source")"
fi
done
If the database is encrypted, back up the key file in the same protected backup set. Never put its contents in a ticket, terminal capture, or evidence bundle:
KEY="/absolute/path/to/secret.key"
cp "$KEY" "$BACKUP_DIR/$(basename "$KEY")"
chmod 600 "$BACKUP_DIR/$(basename "$KEY")" 2>/dev/null || true
Run the keyless doctor check after the database backup. doctor reports
the actual on-disk encryption state and the selected database; it is not a
substitute for a backup:
"$BIN" doctor --db "$DB" | tee "$BACKUP_DIR/doctor-before.txt"
Record the backup identity without recording memory bodies or key material:
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$BACKUP_DIR/$(basename "$DB")" > "$BACKUP_DIR/backup.sha256"
else
shasum -a 256 "$BACKUP_DIR/$(basename "$DB")" > "$BACKUP_DIR/backup.sha256"
fi
2. Update the source checkout
Use a clean, reviewed checkout or pin the exact reviewed tag/commit. For a
checkout that tracks main, the source update is:
git -C "$SRC" fetch --prune origin
git -C "$SRC" switch main
git -C "$SRC" pull --ff-only origin main
git -C "$SRC" rev-parse HEAD | tee "$BACKUP_DIR/source-after.txt"
Do not describe this step as a data migration. The current source owns its
schema initialization behavior when it opens a database; the safe operator
contract is to keep the backup, use an explicit --db, and verify the result.
For the separate legacy v0.1.x format only, inspect the current command help and
use the explicit source/target form when applicable:
"$BIN" migrate --from /absolute/path/to/old.db \
--to /absolute/path/to/migrated.db
Do not run that command for an ordinary current-database upgrade merely because the binary changed.
3. Build and install the binary
Build from the reviewed source, then copy the resulting binary to the required canonical install location. Save the previous binary for rollback before overwriting it:
cd "$SRC"
cargo build --locked --release
mkdir -p "$HOME/.local/bin"
if [ -f "$BIN" ]; then
cp "$BIN" "$BACKUP_DIR/perseus-vault.previous"
fi
cp target/release/perseus-vault "$BIN"
chmod +x "$BIN"
macOS requirement: after every rebuild or replacement of the binary,
re-sign the installed file before running it. This prevents the Apple Silicon
Killed: 9 launch failure. The ad-hoc signature is a local launch workaround;
Developer ID signing/notarization for release artifacts is a separate CI path.
if [ "$(uname -s)" = "Darwin" ]; then
codesign --force --sign - "$BIN"
fi
Verify the installed identity before touching the client configuration:
"$BIN" --version
"$BIN" doctor --db "$DB"
4. Keep the database path and encryption explicit
Pass --db "$DB" to doctor, prepare, serve, and
install-client. Do not rely on implicit default-path precedence during an
upgrade: an existing compatibility database can otherwise be selected when a
different path was intended.
For an encrypted database, keep using the existing key and verify a
key-backed read; doctor itself does not require the key and does not reveal
key material:
"$BIN" doctor --db "$DB"
"$BIN" prepare --db "$DB" --encryption-key "$KEY" \
--task "post-upgrade verification" --json
Do not use init as a generic upgrade step. If a plaintext database is being
intentionally converted to encryption, follow ENCRYPTION.md,
back up first, and use the current command's explicit paths (including
--rekey when existing plaintext bodies are meant to be encrypted):
"$BIN" init --db "$DB" --key-file "$KEY" --rekey
Back up the newly created key immediately. A lost key cannot recover encrypted bodies. Treat a doctor warning about mixed or missing encryption state as an operator action, not as permission to continue with mixed plaintext writes.
5. Preview and install client wiring
Run install-client from the intended agent project directory because
project-scoped config and instruction files are resolved from the current
working directory:
cd "$PROJECT"
Choose one client or use --all-detected as documented by the CLI. The dry run
prints every file and diff it would touch and writes nothing:
CLIENT="claude-code" # or codex, cursor, claude-desktop, hermes, windsurf, vscode, zed
"$BIN" install-client --client "$CLIENT" --db "$DB" \
--hooks --rules --dry-run
Review the dry-run paths, especially the database path, binary path, hooks, and
instructions file. Then run the same command without --dry-run:
"$BIN" install-client --client "$CLIENT" --db "$DB" \
--hooks --rules
If encryption is enabled, append --encryption-key "$KEY" to both commands.
install-client merges rather than overwrites supported config formats. Before
each changed file it writes a <file>.bak-perseus backup; an idempotent no-op
does not rewrite the file. Record the backup paths printed by the command and
copy those backup files into "$BACKUP_DIR" before starting the client again.
6. Restart the client and run an MCP smoke test
Fully exit and restart the MCP host/client after configuration changes. This is needed for it to respawn the server with the new command and database path; reloading only an already-running conversation is not enough.
First exercise the same stdio handshake used by an MCP client. The smoke test uses only the canonical health tool and closes stdin so the server exits cleanly:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"migration-smoke","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"perseus_vault_health","arguments":{}}}' \
| "$BIN" serve --db "$DB"
For an encrypted database, add --encryption-key "$KEY" to the final
serve command. Confirm that the responses contain a successful initialize,
a tools/list entry for perseus_vault_health, and a successful health result;
an MCP error or a missing canonical tool is a failed smoke test. Do not treat a
process that merely starts, or a client configuration that merely parses, as
proof that the intended database was opened.
7. Rollback
Rollback is safest before allowing the new binary to perform writes. Stop the client and server first. Restore the previous binary and re-sign it on macOS:
cp "$BACKUP_DIR/perseus-vault.previous" "$BIN"
chmod +x "$BIN"
if [ "$(uname -s)" = "Darwin" ]; then
codesign --force --sign - "$BIN"
fi
If the new binary opened the database and the schema or data must be reverted, restore the complete pre-upgrade backup set—not just the binary. Only do this when you accept losing writes made after the backup:
cp "$BACKUP_DIR/$(basename "$DB")" "$DB"
for suffix in "-wal" "-shm"; do
backup="$BACKUP_DIR/$(basename "${DB}${suffix}")"
if [ -e "$backup" ]; then
cp "$backup" "${DB}${suffix}"
else
rm -f "${DB}${suffix}"
fi
done
Restore the client configuration from the pre-upgrade copy or the matching
.bak-perseus file produced by install-client, then run doctor and the MCP
smoke test again with the same explicit --db (and key, if applicable). A
binary-only rollback is not proof of database compatibility; when unsure,
restore the verified database backup before reopening it with the old binary.
8. Evidence and privacy checklist
- Record the source commit before and after the update, installed
perseus-vault --version, explicit database path, and command exit status. - Keep the database backup and any SQLite companion files together; record a checksum of the backup, but do not upload the database or its raw output.
- Protect the encryption key backup separately. Never paste key contents,
client tokens, or
--encryption-keyvalues into logs or issue reports. - Save
doctorstatus and the bounded MCP smoke verdict, not full memory bodies, prompts, transcripts, or client logs. - Review
install-client --dry-runbefore writing; retain the printed.bak-perseuspaths and treat config diffs as potentially sensitive. - Confirm that all clients point at the same intended absolute
--dbpath and that no empty replacement database was created by an implicit default. - If exports, derived projections, backups, or host context contain the affected records, handle their retention/erasure separately; a database purge does not erase copies outside the database.
- Remove temporary smoke-test captures and unneeded backup copies according to the operator's retention policy. A checksum is evidence of identity, not evidence that a restore has been tested.
This procedure is local-only. It does not publish, commit, or mutate a remote repository, and it does not claim that a successful process start proves semantic compatibility for every client or database history.