hermes-pubky
Run a Hermes agent whose saved state lives on your own Pubky homeserver. The laptop becomes a working copy, but the agent belongs to you.
Why
Hermes keeps everything it knows in one directory on one machine: SOUL.md,
your memories, its learned skills, your conversations. New laptop, fresh
container, throwaway VM, and the agent starts from nothing. The usual
alternative is a memory SaaS, which makes your agent portable by moving it into
someone else's account.
This is a third option. The homeserver holds the authoritative copy of the agent, a local Hermes reconstructs a working copy, runs it, and saves changes back. What travels:
- instructions (
SOUL.md), user and agent memories - learned skills and their assets
- allowlisted settings
- the conversation database, including tool calls, compaction and rewind history
- a designated workspace: notes, references, outputs
What stays on the machine: API keys, OAuth sessions, the Pubky grant itself, and anything else machine-specific.
Install
uv pip install hermes-pubky==0.2.3
hermes-pubky agent init default
hermes-pubky run default
Run these commands in your activated Hermes environment. Needs Python 3.11–3.13,
Hermes 0.21.3 with conversation schema 30, and a Pubky homeserver on v0.11
or later. This release was verified against Hermes commit
a51143fbbe6ddbc0c7f403d0579c4d75504c6793; use that checkout as described
under Development below. Hermes is distributed from source, not through PyPI.
agent init asks Pubky Ring to authorize one scoped capability, then
publishes the agent's first checkpoint.
On a second computer:
uv pip install hermes-pubky==0.2.3
hermes-pubky agent attach pubky://<owner>/priv/hermes.pubky.app/v2/agents/default/head.json
hermes-pubky run default
Nothing is copied between machines. attach downloads the instructions,
memories, skills, settings and conversation database; workspace documents are
fetched when the agent asks for them. Every run saves a checkpoint when it ends,
and the next run on any machine starts from the newest one.
Hermes plugin packaging
plugin/ is the directory-plugin wrapper for catalog distribution. It declares
the pinned hermes-pubky package dependency, which includes the native Pubky
extension, and adds /pubky setup help when enabled. Managed agents still
start through hermes-pubky run; enabling the companion does not sync an
ordinary Hermes profile or select its memory provider.
Once this directory is published in the repository, install it with:
hermes plugins install MCarlomagno/hermes-pubky/plugin
hermes plugins enable hermes-pubky
Current Hermes installers install the pinned dependency from
plugin/pyproject.toml into the Hermes environment. If dependency installation
is disabled or fails, install it explicitly with the pip command above.
The installer also displays setup instructions.
The catalog entry is a setup helper. Running a managed agent syncs its instructions, memories, skills, conversation database snapshots, portable configuration, and workspace to your remote Pubky homeserver. The homeserver operator can read that data; it is not end-to-end encrypted.
For a catalog submission, set subdir: plugin and requires_hermes: ">=0.21.3".
The setup helper remains available on newer Hermes versions. The managed
launcher still requires the verified Hermes 0.21.3 / schema-30 runtime and
rejects unverified runtimes before touching agent state.
Keep the wrapper's manifest version, project version, and exact package
dependency aligned with the published release. The wrapper never downloads or
updates code at import time.
Upgrading an existing agent
Stop the agent and sync its latest work with the old runtime before upgrading. Install hermes-pubky 0.2.3 in the verified Hermes 0.21.3 environment on every computer that runs the agent. Schema-22 conversations from Hermes 0.19.0 are upgraded using Hermes' own migrations on a staged copy during capture or restore. The next checkpoint saves schema 30; old checkpoints remain in history. An upgrade failure leaves the working database unchanged. Other unverified schemas are refused. After the new checkpoint is saved, use the newer runtime on every device; hermes-pubky 0.2.1 cannot read schema 30.
Commands
hermes-pubky run <id> [--resume SESSION] [--offline] [--query TEXT]
hermes-pubky agent init <id> [--from-hermes-home PATH] [--workspace PATH]
hermes-pubky agent attach <pubky-uri>
hermes-pubky agent list
hermes-pubky agent status <id> [--json] [--offline]
hermes-pubky agent login <id>
hermes-pubky agent logout <id>
hermes-pubky agent sync <id> [--prefer local|remote]
hermes-pubky agent files <id> list|fetch|import|remove
hermes-pubky agent history <id>
hermes-pubky agent restore <id> <snapshot-id>
hermes-pubky template init|inspect|publish <directory>
hermes-pubky template adopt <agent-id> <pubky-uri>
hermes-pubky template update <agent-id>
agent sync seals whatever changed locally, including files added by hand or
with agent files import, then publishes everything pending. agent logout
revokes the grant at the homeserver and says so only when that succeeded.
Exit codes: 0 done, 1 usage, 2 saved locally but not yet on the homeserver, 3 conflict, 4 authorization needed, 5 quota, 6 integrity or unsupported runtime.
What it can reach
agent init requests one capability:
/priv/hermes.pubky.app/v2/agents/<id>/:rw
Read and write inside that agent's own directory. No root capability, no access to your other apps. The launcher also refuses paths outside that directory locally, before a request leaves the process, and keeps the grant out of the Hermes child's environment entirely.
Publishing a template needs its own separate capability under /pub/. An
agent's grant can never publish.
A grant holds one session at a time. Authorize each computer with its own
agent login rather than copying a grant between machines, or each will keep
signing the other out.
Read this before you store anything
Your homeserver operator can read your agent. /priv is access-controlled, not
encrypted. If you self-host, that operator is you, otherwise assume whoever runs
your homeserver can read your instructions, memories and conversations.
Credentials are never uploaded, but a conversation can contain anything you or a
tool put in it.
One writer at a time. Read from as many machines as you like, but stop and sync
on one before writing from another. If the homeserver's checkpoint moved while
this machine has unsaved work, run stops before starting the agent, installs
nothing over your work, and asks you to choose with agent sync --prefer local|remote; whichever side you drop is preserved under recovery/ first.
Offline works. Startup seals anything a previous run left unsaved, then refreshes
from the homeserver with a five-second budget. Offline runs chain their
checkpoints, survive restarts, and publish in order at the next sync, retrying
with backoff capped at 60 seconds. A run that could not reach the homeserver
exits 2 and says saved locally; not yet saved to homeserver. A run whose
conversation database cannot be captured exits 6 and does not claim anything
was saved.
A cold attach downloads the conversation database in full. It is streamed and reassembled on disk, never held in memory, but a large history takes time. Workspace documents stay remote until requested.
History is kept. Older checkpoints remain recoverable and count against your homeserver quota. Removing a file drops it from the current inventory, not from history; this is not secure erasure.
Not in 0.2: multi-device merge, client-side encryption, portable credentials, semantic retrieval, Windows wheels, and harnesses other than the verified Hermes 0.21.3 runtime.
How it fits together
hermes-pubky run <id>
-> read and verify the remote checkpoint
-> materialize a dedicated HERMES_HOME and workspace
-> start Hermes as a child, with HERMES_HOME set before it imports
-> the in-process plugin reports changes and serves two workspace tools
-> the supervisor owns the lock, the journal and every network call
-> final capture and sync after the child exits
Storage is content-addressed. A checkpoint is an immutable snapshot document
naming immutable objects; only head.json is mutable, and it moves last, after
every object it names has been verified as recorded. The homeserver offers no
conditional write, so this is ordering, not a lock: concurrent writers remain
unsupported and are detected rather than merged.
Locally, the journal tracks one base: the checkpoint the working copy corresponds to. Sealing a checkpoint advances it, installing one advances it, publishing never does. That is what lets a second run continue the first and offline runs form a chain.
/priv/hermes.pubky.app/v2/agents/<id>/{head,snapshots/*,objects/*}.json
/pub/hermes.pubky.app/v2/templates/<id>/{head,snapshots/*,objects/*}.json
Markdown is stored as Markdown and JSON as JSON, so what is on your homeserver is readable there. Files over 1 MiB, and the database always, are split into 1 MiB chunks.
Development
uv venv --python 3.11 .venv
git clone https://github.com/NousResearch/hermes-agent.git /tmp/hermes-pubky-runtime
git -C /tmp/hermes-pubky-runtime checkout a51143fbbe6ddbc0c7f403d0579c4d75504c6793
uv pip install --python .venv/bin/python maturin pytest -e /tmp/hermes-pubky-runtime
PYO3_PYTHON="$PWD/.venv/bin/python" .venv/bin/maturin develop
.venv/bin/pytest # Python tests; the Hermes ones skip if it is absent
cargo test --lib # Rust unit tests
tests/test_supervisor.py runs the whole launcher against a fake homeserver
and a stand-in child: two runs in a row, an attach on a second machine, offline
chains, a moved head over local edits, a corrupt database, a failed restore.
Those are the scenarios that matter; the rest of the suite guards the wire
format and the policy boundaries.
The end-to-end and acceptance suites need PostgreSQL for the homeserver and the well-known testnet ports free:
docker run -d --name hermes-pubky-pg \
-e POSTGRES_USER=test_user -e POSTGRES_PASSWORD=test_pass -e POSTGRES_DB=postgres \
-p 5432:5432 postgres:18-alpine
export TEST_PUBKY_CONNECTION_STRING="postgres://test_user:test_pass@localhost:5432/postgres?pubky-test=true"
cargo test --test v2 -- --ignored --test-threads=1
A real managed Hermes turn, against a local fake model so no credentials are needed:
python scripts/check_managed_hermes_integration.py
Verify the catalog wrapper in a fresh environment using the candidate wheel:
maturin build --release --out dist
python scripts/check_plugin_install.py --hermes-source /tmp/hermes-pubky-runtime --wheel-dir dist
The full recovery scenario through the shipped command line, one agent across two machines, ending with the grant revoked (so each run needs a fresh fixture):
cargo run --example testnet_fixture > /tmp/fixture.json &
HERMES_PUBKY_TESTNET=1 python scripts/check_managed_handoff.py --fixture /tmp/fixture.json
Pinned upstream versions, changed only as a deliberate compatibility decision with tests to match:
| Hermes | Hermes 0.21.3 at the verified commit above, conversation schema 30 |
| Pubky SDK | v0.11.0 commit 6a14bdb8fa2e30ef4e4b241fcdd3992c453d2378 |
Cargo.lock is committed. tests/test_hermes_contract.py holds the upstream
contract as executable tests and fails if the installed package drifts from it.
License
MIT