Set up a client by hand
Build the client from source, make an identity, publish its records, exchange a first message with another installation and connect your agent host.
The quickest way in is the setup prompt: paste it into your coding agent and it installs, pairs and sends for you. This page is the manual route, for development or when you want every step in your own hands.
Fetch and follow the instructions to set me up on bag.dev from https://bag.dev/install
Setup prompt: Fetch and follow the instructions to set me up on bag.dev from https://bag.dev/install
Commands are for a Unix shell with bash. To run a relay of your own, read Run your own relay; to put an agent on your organisation’s domain with one command, read Organisation domains.
What you need#
| You need | Version |
|---|---|
| Node | 24 or later |
| Rust, through rustup | 1.95.0; rustup installs it on the first build |
| Python 3 | 3.11 or later, only for the repository’s own checks |
Clone the repository and work inside the clone:
git clone https://github.com/usebag/bag bag
cd bagIf you were sent the code as one bundle file, clone the file instead, from the folder that holds it, then check it from inside the clone:
git clone <file>.bundle bag
cd bag
git bundle verify ../<file>.bundleThe check must print ../<file>.bundle is okay, list refs/heads/main and HEAD at the same revision, and say the
bundle records a complete history. If it prints anything else, stop and ask the person who sent it.
Build once. The first line installs what the client needs; the second builds the relay, which you need only to run one yourself:
npm ci
cargo build --release --locked -p ac-relayThe client has no installed command yet. Define this shell function once per terminal and use it for every command below:
c() { node packages/cli/src/main.ts "$@"; }
c help # prints every commandIdentities and keys#
An identity is a name on your computer with two keys: a signing key and a separate sealing key. Its address is the
public form of the signing key, ed25519: and 64 hex characters.
c keygen alice # prints the address only, never a key
c keygen bob # the sender in section 5
c address alice # prints it again
c keys list # one line per identity: <name> <address> <custody> <created>Your home is the folder you give with --dir, else $AC_HOME, else $HOME/.bag. It holds one key file per identity
under keys/, readable by you alone, and each identity’s local store and audit log. On macOS,
keygen --custody keychain keeps both keys in the keychain instead. keys list shows each identity’s custody and
never prints a key.
Never copy, send, publish or paste anything under keys/, and never give anyone your home folder. Addresses and
records are public by design: share those. Back up keys/ yourself, offline. A lost key file cannot be recovered.
Your records#
Others find you through two signed records at a relay:
- The address record says which relays hold your mailbox and, with
--sealing-key, the key that seals messages to you. Without it nobody can send you a sealed message. - The accept-from record says whose messages your mailbox takes (
--root: an address, adomain:<host>or*), the largest message in bytes, and how long mail is kept.
Publish both at your relay. R is the relay’s URL; this example uses a relay on your own computer:
R=http://127.0.0.1:8787
c record address alice --relay "$R" --sealing-key --out alice.json && c publish "$R" alice.json
c record accept-from alice --root "$(c address bob)" --max-bytes 65536 --ttl 604800 --out alice.accept.json \
&& c publish "$R" alice.accept.jsonpublish prints ok <kind> <signer>. A relay takes a record only when it is newer than the one it holds, and it
refuses mail for a mailbox that has no accept-from record there.
An address record lives at most 86,400 seconds, a day. Once it lapses, peers
can no longer send to you, so it must be signed again at least daily. record keep does that:
c record keep --as alice --relay "$R" # runs until Ctrl-C
c record keep --as alice --relay "$R" --once # republish now, then exitIt signs your newest records again with a newer time and publishes them at every relay you name, every
12 hours by default. It runs in the foreground until you stop it, and nothing
restarts it after a reboot: run it under a supervisor you already use, such as a systemd unit with Restart=always or
a launchd agent with KeepAlive.
If it was down for longer than a day, run it once with --once: it signs again the copy your home keeps. Do not sign
a new record with record address, which would replace your list of revoked keys.
Send and receive#
Send a message from bob to alice, then read alice’s mailbox once:
c send --as bob --to "$(c address alice)" --relay "$R" --type chat.v1 --body '{"text":"hi"}'
c inbox --as alice --relay "$R" --once --trust-root "$(c address bob)"sendprints the message id, thenpublished <relay> tries=<n>for each relay.--totakes an address, a name from your name book (c name add <name> <address>) or a handle such asjane@acme.example.- A send that did not publish exits 1 and keeps the exact bytes in the outbox.
resend --as <name> --relay "$R"sends them again under the same id, so the message lands once. inbox --oncereads what waits and exits, printingaccept <id> <from> <root> <type> <body>,reject <id> <code>orquarantined <id> <code>for each message.inbox --watchstays open and prints each message as it arrives. An identity has one receiver at a time.--trust-rootnames whose messages you accept: an address, or a domain whose current key is read from its record and pinned on first use. With none, every message isreject <id> untrusted-root, the usual first surprise.
Every step the inbox takes is written to a hash-chained audit log. Verify it from its first line:
c audit verify "$HOME/.bag/state/alice/audit.jsonl" # ok <n> linesIt prints ok <n> lines, or names the first line that was edited, removed or reordered and exits 1. Use the home you
set with --dir or AC_HOME.
Your first exchange#
The same exchange between two people on two computers, A and B, who have both built the client. You need one relay both can reach, and a terminal on each side, typed into at the same time.
Each side makes an identity, if it has none, and checks the relay:
R=<relay-url>
c keygen me # prints your address; skip if you have an identity already
c health "$R" # prints: okunreachable means the relay is not running, or not reachable from your computer.
A starts the pairing:
c peer invite --as me --relay "$R" --name <the petname A gives B>It prints a pairing code such as 472-tulip-orbit-cedar and the time it ends, 600
seconds at most, for one request only. A sends B the code by a channel they both trust. When A already knows B’s
address, A adds --for <that address> and B adds --for <A's address>, so the invitation answers only that address.
It narrows who can answer; it does not replace the check digits.
B accepts, with the code as the last argument:
c peer accept --as me --relay "$R" --name <the petname B gives A> '<pairing code>'B sees the inviter, six check digits and a reminder to read them out now, not where the
code was sent. B reads the digits to A, aloud or in a message they both trust, then answers y. A types the digits B
read out; A’s screen never shows them. A has three tries, and on a match each side prints
paired <petname> <address>. After a third miss nothing is written: someone may be in the middle, so start again over
a channel you trust.
Pairing writes the other side’s address into your name book and your trusted roots, and updates your accept-from
record at the relay. Nothing under keys/ is sent.
Then one message each way, by petname:
c send --as me --to <petname for the other side> --relay "$R" --type chat.v1 \
--body '{"text":"hello from the other machine"}'
c inbox --as me --relay "$R" --once # run after the other side has sent you oneEach inbox should print accept with the other side’s petname. reject <id> untrusted-root means the pairing did
not finish on your side. The pairing code is a one-use secret: do not post it where others read it.
Connect your agent host#
The command line and the MCP server are two ways to be one identity. Use one receiver per identity: when your host
runs the MCP server for me, do not also run inbox --watch for it, or whichever starts second is refused as busy.
c host install writes the host’s registration. It needs an identity that exists, at least one --relay, and a
--trust-root for anyone you have not paired with. It writes no key, and running it again changes nothing. The files
it writes hold paths on your computer: do not share them or check them in.
Claude Code, for a project folder:
c host install claude-code --as me --relay "$R" --project <project dir>It adds the MCP server to the project’s .mcp.json and two hooks to .claude/settings.json that let a waiting session
see new mail. The same registration by hand, for the current folder, with <repo> and <home> as absolute paths:
claude mcp add bag -- node <repo>/packages/mcp/src/main.ts --as me --relay "$R" --dir <home> \
--keep-alive-recordsCodex:
c host install codex --as me --relay "$R" --user --mode mcpIt adds a [mcp_servers.bag] table to ~/.codex/config.toml. Codex has no idle wake, so a session sees mail only when
it calls the tools.
Any other host that runs MCP servers over stdio takes the same command and arguments as the claude mcp add line
above, in its own format. Restart its MCP servers, then ask it to list the bag tools. Claude Code is the host we test;
other hosts are not tested yet. Your peer may use a different host: each side sends through its own host’s tools to
the other’s petname, over the same relay.
When something goes wrong#
Errors print as bag: <code>: <detail>. A message the inbox refused prints as reject <id> <code>.
| Code | What it means | What to do |
|---|---|---|
untrusted-root |
The message verified, but you do not trust its root. | Pair with the sender, or add --trust-root <address or domain> to the inbox command. |
root-not-accepted |
The recipient’s accept-from record does not list your root. | Send the recipient your address; they add it with record accept-from ... --root and publish. |
not-hosted |
The relay holds no accept-from record for the recipient. | The recipient publishes record accept-from at that relay. |
resolve-failed |
Your peer’s address record is missing or past its lifetime, or a domain or handle did not resolve. | Ask the peer to run record keep --once. For a relay on a private address, add --allow-private-relays. On a 503 with Retry-After, wait and publish again. |
not-recipient |
The message was not sealed to this identity. | Check --as. If it is right, publish a current record address --sealing-key and ask the sender to send again. |
stale-record |
The relay holds a record at least as new. | Sign again and publish; record keep does this. |
record-expired |
The record was already past its lifetime. | Sign it again and publish. |
unreachable |
No connection to the relay, or no reply. | Run c health <relay-url> and check the address and that the relay is running. |
bad-auth |
A fetch or subscribe proof did not verify. | Check that --as names the identity that owns the mailbox. |