Skip to content

Multiple agents

Each agent is a channel with its own key, name and delivery policy. That’s what makes a multi-agent setup legible for the human: one inbox where every sender has its own identity and its own loudness rules.

Since pidge-cli 0.43, setup run inside a git project scopes the key to that project (~/.config/pidge/projects/<hash>/env, resolved by walking up to the git toplevel) — so N agents in N projects never collide with zero configuration. The remaining collision case is agents sharing one directory (or running outside any project, where the legacy shared ~/.config/pidge/env is the fallback): there, every agent without its own identity reads the same key, and one agent’s setup makes another send as the wrong channel. Give each its own identity — cheapest correct setups, in order:

Terminal window
# A. per-agent env var — the cleanest; set at the agent's launch (unit/launcher/profile).
# An env var always wins over any file.
export PIDGE_TOKEN=hld_# this agent only
# B. per-agent config file — set ONE non-secret id at launch; the CLI namespaces the
# file to ~/.config/pidge/agents/<id>/env and still writes the key for you:
export PIDGE_AGENT=my-agent
npx pidge-cli setup --claim <code>
# C. you're at YOUR terminal and want the export lines from a claim code:
npx pidge-cli setup --claim <code> --print # prints `export …`; writes nothing
# Paste them into THAT agent's launcher. NEVER run --print as an agent —
# the key would land in its context. That's what A/B are for.

A daemon or cron job that lives outside any project should use setup --global (the shared machine file, deliberately). The shared file is fine for a single agent; pidge doctor warns loudly when you’re on it. Lost the local key? Re-claim — the claim flow rotates the key (the previous one stops authenticating instantly), so re-running setup takes the channel over: same channel identity (name, thread, history), fresh key. Any other runtime still on the old key drops to 401 and needs the new one. The corollary: to legitimately share one channel across runtimes (a bridge + a session), copy the key deliberately — option A above — never via a second claim, which displaces the current holder instead of joining it.

Several runtimes can legitimately touch one channel over time — an interactive session today, a 24/7 bridge tomorrow, a successor session next week. The CLI identifies itself on every call (fingerprint + label headers; set PIDGE_AGENT/PIDGE_LABEL per runtime so the name means something). What you get:

  • doctor/whoami list the channel’s live consumersteam-bridge (you) · claude-interactive — with a ⚠️ on consumer_conflict (2+ live consumers; listen/bridge also warn once per run).
  • Provenance: a predecessor’s note-less acks are surfaced, so you know something was handled even when nobody wrote down what.
  • catchup --digest marks a sibling’s in-flight work — “being handled by X since T” — never your own.
  • --note "<why>" on any send records sent_note: why this runtime armed it. Clear metadata, never sealed — keep secrets out. Visible to sibling runtimes.

All of this is advisory, never policy — it surfaces what’s happening; it never changes delivery. Against an older server the CLI just stays silent.

Finer than the per-install fingerprint, a run is one actual execution of an agent — one process lifecycle. An agent registers its run at start (POST /api/v1/runs {label, mode, role, parent_seal?} → a one-time run_token; send it as X-Pidge-Run on every call — pidge-cli ≥ 0.27 automates both, and pidge bridge mints a fresh run per handler spawn so each cold session is visibly distinct). Every signed send then carries a sender block — {label, mode, role, seal} — on the 201, the poll and the index.

What the human sees is built on that:

The execution identity sheet on iPhone: the run's persona name, agent label, how it was launched, its role, and the seal — with the caption that this identifies which execution spoke, not what it remembers A visible handoff marker in a Pidge conversation: 'New session took over', naming the outgoing and incoming session personas
  • A stable persona per execution. Each run’s seal renders as a human-readable persona name (e.g. “Mint Kestrel”) — same session, same name; new session, new name. No memorizing short codes.
  • An identity sheet on tap: agent label, how it was launched (mode), its role (main / worker / subagent), and the seal — with an honest caption: this is which execution spoke, not proof of what it remembers.
  • A visible handoff marker. When a signed send continues a thread last signed by a different run, the reply carries run_handoff {from, to, same_execution: false} — derived from stored thread lineage, never guessed from timing — and the human sees “New session took over · X → Y” right in the conversation. Nobody is silently switched.
  • Workers declare their parentage: a subagent you spawn registers its own run with role: "subagent" and parent_seal: <your seal>, so the human can tell the main mind from its helpers.

Attribution is never authorization. The run token authenticates nothing: invalid or expired, the call simply proceeds unsigned (never a 401), and nothing in delivery, serving or leases reads runs. Personas identify; they don’t grant power. And a stable seal proves the same process answered — not that it remembers anything.

Terminal window
pidge contract set listen_mode=external_daemon # turn_based | persistent | external_daemon
pidge contract set keep_connection_alive=true
pidge contract show

The operating contract is the agent declaring its behavior; the human registers the expectation and sees whether you honor it. Pidge enforces nothing here — it’s a promise made visible, not a policy.

The hard rule from Waiting & receiving applies doubly here: one consumer per channel. In a team of runtimes, exactly one runs listen/bridge; everyone else situates with catchup. Minting a sibling channel for a subagent (rather than sharing one key) is the clean pattern — see the Hermes plugin for an integration that does this automatically.