AgentBridge
by Joseamica
README.md
# AgentBridge
Ask another person's coding agent a question — across different machines, networks and
subscriptions — without waiting for that person to become available, and without either of you
running a server.
> **Language note:** the code, comments and this README are in English. Everything the tool
> *says to a human* — CLI output, errors, the agent-facing channel notices — is in Spanish,
> and so is the documentation for the people actually running it. That was a deliberate product
> choice, not an oversight.
>
> **¿Español?** La guía paso a paso para las dos personas que lo van a usar está en
> [`docs/inicio-rapido.md`](docs/inicio-rapido.md).
## The problem
You're deep in a task and you need one fact that lives in someone else's head — or, more often,
in someone else's repo, which their agent already knows. So you message them. They read it an hour
later, ask *their* Claude Code, copy the answer back, and you've lost the afternoon.
The bottleneck isn't the answer. It's the human in the middle relaying it.
AgentBridge lets your agent ask their agent directly, with the other person's explicit,
revocable permission, inside a folder they control.
## How it works
```mermaid
flowchart LR
A["Your Claude Code<br/>(ask_contact)"] -->|sealed wrap| B1["Public Nostr boards<br/>(five, by default)"]
B1 -->|sealed wrap| C["Their Claude Code<br/>locked-down session"]
C -->|sealed wrap| B1
B1 -->|answer| A
```
1. Each person runs `setup` once: it creates a key on their own machine and writes their profile.
There is no account and no server of ours.
2. They exchange links (`agentbridge:nprofile1…`). One asks for permission with `connect`; the
other sees it with `requests` and decides with `approve` or `reject`. Permission is
**directional**, and only whoever granted it can take it back, at any time, with `revoke`.
3. Questions and answers travel as NIP-59 sealed wraps through public Nostr boards. A board sees
an encrypted envelope, the ephemeral key that published it, **the recipient's key it is
addressed to**, its size and its timing. It never sees the content, and it never sees who wrote
it — but a board operator can watch which key receives envelopes, and correlate sizes and
timing across boards.
4. The responder's agent runs in a dedicated, permission-restricted Claude Code session that can
read what its owner chose — one folder by default — and nothing else, and answers with a single
`reply` tool.
5. The answer comes back through `check_answer` or `ticket`. Nobody had to be online at the same
moment; a question is retried for up to seven days.
## Security model — read this before you install it
The responder's session runs **unattended**, and an incoming question is **untrusted input
written by someone else**. The design assumes that and is built around one rule:
> A concrete rule holds. A prose rule does not.
Adversarial testing on this project showed a persona instruction ("don't read anything outside
this folder") gets applied at answer time and can be talked around, while a deny rule or a path
fence holds. So the boundary is enforced by configuration, not by asking the model nicely.
**What the responder session cannot do**, in every permission mode:
- Run shell commands, edit or write files, fetch the web, or spawn subagents — all denied.
- Read anything outside the folders its owner chose. Enforced by
`permissions.blockReadsOutsideWorkingDirectories`, which must be nested inside `permissions`;
a copy at the top level of the settings file is accepted and silently ignored. The wider scopes
below add directories to `permissions.additionalDirectories`; they never turn the fence off.
**What it can do — the part you must accept:**
- **Everything it can read is readable by anyone allowed to ask you.** In the shared folder that
includes a key file, an `id_rsa` or a password written in a document; only files named `.env`
or `.env.*` are denied there in every scope. The correct mental model is: *that folder is public
to anyone allowed to ask you.* Curate it deliberately. Do not point it at a working repo.
- **Project configuration that lands in that folder later** — `.claude/settings.json` hooks,
`.mcp.json`, `.claude/agents`, `.claude/skills`, `.claude/commands`, `CLAUDE.local.md` — takes
effect at the next session start. The `doctor` command flags all of them, and an `AGENTS.md`
too: Claude Code 2.1.282 does not read it at all, but a later version might. Nothing prevents a
sync client or a `git pull` from placing them.
### What the responder can see: three scopes
`setup` asks the answerer once, for every contact at once (there is no per-contact scope):
| Scope | Readable | Denied on top of the fence |
| --- | --- | --- |
| **1 — one folder** (default, recommended) | the shared folder | `.env` and `.env.*` in it |
| **2 — several folders** | the shared folder plus the folders the answerer names | the caja fuerte; `.env`, `.env.*`, `*.pem`, `*.key`, `*.p12`, `*.pfx` inside each extra folder and the shared folder |
| **3 — the whole personal folder** | the shared folder plus the home directory | the caja fuerte |
The **caja fuerte** ("safe") is a fixed list in code (`CAJA_FUERTE_HOME` in
`packages/cli/src/commands/setup-responder.ts`) that nobody, the answerer included, can open
through `setup`: the
AgentBridge identity folder and the dedicated profile wherever they live, the owner's everyday
Claude (`~/.claude`, `~/.claude.json*`, the desktop app's configuration), the best-known credential
stores (`~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.azure`, `~/.config/gcloud`, `~/.kube`, `~/.docker`,
`~/.config/gh`, `~/.npmrc`, `~/.pypirc`, `~/.netrc`, `~/.git-credentials`, cargo credentials,
`~/.terraform.d`, `~/.config/op`), shell and REPL histories, the macOS keychain and browser
profiles, their Linux equivalents, all of `~/AppData` on Windows, and every `.env`, `.env.*`,
`*.pem`, `*.key`, `*.p12` and `*.pfx` under the home. It closes the best-known places, **not every
secret a person has**: a password written in a document, mail and chats saved on disk, or a key
file with an unusual name stay readable in scope 3. It also covers only the usual locations: an
everyday Claude run with a custom `CLAUDE_CONFIG_DIR`, or a dedicated profile left over from an
older `--profile`, is not in the list.
Scope 3 needs a typed `CONFIRMAR` after a screen that says, plainly, that anyone the answerer
approves can then ask about any file in their personal folder outside the caja fuerte. **On
Windows only scope 1 exists for now.** Scope 2 needs each extra folder's absolute path in a rule,
and how Claude Code anchors a drive-letter path is unverified. Scope 3 rests on the `~/…` rules,
which have never been checked on Windows, and its consent screen tells the person the caja fuerte
stays closed: a promise nobody has checked is not made at the moment they type `CONFIRMAR`. Running
section 8 of the acceptance runbook on a Windows machine is what would lift this.
**Every rule that must hold outside the working directory is anchored** — `~/…` for the home,
`//<absolute path>/…` per extra folder. That is the one fact this design rests on: on Claude Code
2.1.282, with the home added as a readable directory, an unanchored `Read(**/.env)` did not stop a
direct read of `~/proj/.env`; `Read(~/**/.env)` did. The same binary also showed that Grep and Glob
respect these denies, including when they recurse from an allowed ancestor; that case variants and
symlinked spellings of a denied path are refused too; and that nothing from an extra folder's own
configuration (`CLAUDE.md`, `.claude/`, `.mcp.json`, `CLAUDE.local.md`, `AGENTS.md`) reaches the
model. Those are behaviours of one Claude Code version, so the acceptance runbook re-checks them
against the installed one.
The scope is stored in `responder.json` (version 2; a 0.3 version-1 file reads as scope 1). `setup`
rewrites the profile's `settings.json` from it on every run, and writes `.agentbridge-scope.md` into
the shared folder so the model knows its reach — the persona `CLAUDE.md` imports it. `responder`
refuses to start, and `doctor` fails, when `settings.json` does not match the saved scope exactly:
a missing caja fuerte rule, or a readable directory nobody chose.
`npx -y @joseamica/agentbridge@latest doctor` is how you verify all of this on a real install,
including that every board you use actually accepts and returns what you publish. Run it before
you trust it.
## Privacy: what's guaranteed and what isn't
**Guaranteed:**
- Nobody outside the two of you can read questions, answers, names or notes.
- The public event never names the sender: anyone watching a board sees "an envelope for key X",
signed by a single-use key that is thrown away right after.
- Nobody can impersonate a contact: every message is only accepted if the seal is signed by the
expected key.
**Not guaranteed:**
- **Hiding who talks to whom from a board's operator.** A board can correlate the IP that
publishes an envelope for X with the IP that later reads X's envelopes, and — where it requires
NIP-42 AUTH — the actual key doing the reading. Copying the same envelope to several boards lets
an operator correlate across boards too.
- **Forward secrecy.** If your key is ever stolen, whoever holds it can decrypt any envelope
addressed to you that a board kept.
- **Deletion.** The NIP-40 expiration tag asks boards to delete after 7 days; it does not force
them to.
- **Availability under a targeted attack.** Rate limits contain casual abuse; an attacker with
real resources can degrade service for one specific key, including crowding out legitimate
questions.
- **Protecting the key from other programs you run.** The 0600 permission on `identity.json`
keeps other users of the machine out, not other programs of yours. The locked-down responder
session cannot read it — in scope 1 because it lies outside the fence, in scopes 2 and 3 because
its folder is denied by name — but an ordinary Claude Code session on the same machine could.
And the one that matters most in practice: **everything the responder can read is readable by
anyone you've given permission to ask you.** In the shared folder that includes a stray key file;
in scope 3 it is every file in your personal folder outside the caja fuerte. Say it out loud to the
other person before either of you puts anything in there.
## Requirements
- Node.js >= 22.13
- Claude Code on the responder's machine (developed against 2.1.270; the permission behaviours the
0.4 scopes rest on were verified on 2.1.282)
There is nothing to deploy, host or pay for. Both machines only ever talk *out* to public Nostr
boards; neither is exposed to the internet, and there's no server of ours in the middle.
## Two people, two roles
| Role | Who it is | What they do |
| --- | --- | --- |
| **Answerer** | the person whose knowledge you want | Leaves a Claude Code session running in a locked room, with copies of only the files they chose to share. |
| **Asker** | the person with the question | Asks from their own Claude Code, or from the CLI. |
Permission is **directional**. Ana being allowed to ask Dev does not let Dev ask Ana. If you want
both directions, do the grant step twice, once each way. `revoke` is also directional: only the
person who granted permission (the answerer) can take it back instantly with `revoke <name>`. The
asker simply stops asking — there's no `revoke` for that side.
The "locked room" is the important idea. The answerer picks one folder and copies into it only
what they're willing to share. By default their agent can read that folder and **nothing else on
the machine** — that's enforced by configuration, not by asking the model nicely. Everything in the
room is fair game, so the room is curated on purpose. It is not your working repo. The answerer can
widen the room to more folders or to their whole personal folder minus the caja fuerte — see
[What the responder can see](#what-the-responder-can-see-three-scopes).
```mermaid
sequenceDiagram
participant A as Ana's Claude Code
participant B as Public Nostr boards
participant D as Dev's locked session
Note over A,D: one time: both run setup, Dev approves Ana's connect request
A->>B: sealed wrap: ask_contact "which timeout applies to card reads?"
B->>D: Dev's channel picks it up
Note over D: reads only the shared folder
D->>B: sealed wrap: reply
B->>A: check_answer returns the answer
```
Nobody has to be online at the same moment. If Dev's session is down, the question waits — on the
boards, retried automatically — until he starts it again.
## Quickstart
### The guided way (recommended)
On each machine, run one command and answer its questions — in Spanish, like everything else a
human sees in this tool:
```bash
npx -y @joseamica/agentbridge@latest setup
```
It creates your key and profile if they don't exist yet, asks whether you're going to answer
questions, ask questions, or both, and — before it ever asks you to name a folder to share —
explains in plain language what putting one there means: everything inside becomes readable by
anyone you let ask you, including a stray `.env` or key file.
Every choice in this flow is an arrow-key menu with a marked default: press Enter to take it, or
move with the arrows. Four things are still typed, on purpose: your name, `CONFIRMAR` (a deliberate
act, never a reflex Enter), the other person's link when you paste it, and the Enter that opens
Claude's login. Colour marks the four things that matter — the link to send, what worked, what's
still pending, and what blocks you — and never colour alone: every coloured line still carries its
own word or mark. Folder questions open the operating system's own "choose folder" window instead of
asking for a typed path; closing that window on purpose goes back to the same menu, and a window
that fails to open at all (no matching tool, no display, over SSH) falls back to the typed path
question 0.4 asked. While the window is open, anything typed in the terminal is thrown away, and
Ctrl+C closes the window and ends `setup` the way it does at any question.
On a terminal that can't draw a menu — `TERM=dumb`, or `setup` with its output piped — each menu
becomes a typed question with numbered options (`Escribe 1 o 2 (Enter para elegir 1)`). The folder
question is then a typed two-option question, not 0.4's path question: its second option still
opens the window, and only a window that can't open asks for the path. With no terminal at all
(input piped), `setup` refuses to start, as in 0.4. The macOS window has been seen working with the
shipped script (the owner, 2026-09-27: one cancel, one choice); the same window on Windows and
Linux has never run on a real machine.
Three folders it refuses outright, with no confirmation available: your own home directory; any
folder containing your AgentBridge identity or the dedicated responder profile; and a path that
is not a directory at all (a file, a broken symlink, or something it could not inspect). Seven
more it will use only after you type `CONFIRMAR`, and it names which one fired: a `.git`
repository inside, credential-shaped filenames, symlinks it did not follow, a `node_modules` it
did not read, project configuration already sitting there (`.claude/settings*.json`, `.mcp.json`,
`AGENTS.md`, `CLAUDE.local.md`, `.claude/agents|skills|commands`), a tree too deeply nested to
walk fully, and a tree too large to walk fully — the last two because it cannot then promise none
of the others is hiding further in. It never creates that folder silently.
Next it asks what the answering agent can see — the shared folder only, that folder plus others the
answerer names one by one, or the whole personal folder minus the caja fuerte, behind a typed
`CONFIRMAR` (on Windows, only the first for now). Each extra folder goes through the same checks,
less two that are not true of it: project configuration, which does not load from an additional
directory, and symlinks, whose real target Claude Code holds to the same fence and caja fuerte.
Enter keeps what the machine already has.
Then it *performs* the rest instead of printing it. It opens Claude's login in the dedicated
profile — the browser opens, you type your password, you come back — and afterwards checks for
itself whether a session actually exists. It copies your link to the clipboard. It registers the
MCP server if you say yes. It runs every `doctor` check and speaks up only about the ones that
need you to do something, not the full diagnostic. It ends with a short verdict — what's ready, what's still
pending — and then, if nothing is blocking, offers to start answering right there: say yes and that
terminal becomes the responder.
`setup` is a thin conductor: every step it takes is one of the commands documented below
(`connect`, `setup-responder`, `doctor`, `responder`, `claude mcp add`, `claude auth login`) — it
never reimplements their logic. If it can't run interactively (no TTY — a script, CI, a redirected
pipe), it says so immediately and prints the equivalent commands instead of hanging.
Nothing it prints is a line you have to paste to finish installing, on any platform, and
`tests/acceptance/docs.test.ts` holds these docs to the same rule. The first real Windows install
of 0.2 ended in seven pending items, two of which were bash — an inline environment-variable
assignment in front of `claude`, and a path to a shell script — on a machine where neither could
run.
Read on if you want to understand exactly what each step does, run one by hand, automate it, or
fix something `doctor` flagged.
### On both machines
Node >= 22.13 is required, to run `npx` — nothing else. The responder also needs Claude Code; the
asker only needs it if they want to ask from inside their agent rather than from the terminal.
Nothing to clone, build, or alias. Every command below runs through `npx`, which fetches
AgentBridge the first time it's used and reuses it after that:
```bash
npx -y @joseamica/agentbridge@latest --help
```
(Hacking on AgentBridge itself instead of installing it? See
[Running the CLI from a local clone](#running-the-cli-from-a-local-clone) below.)
### Manual, step by step
Ana is going to ask; Dev is going to answer. Swap the names for your own.
**On both machines — create an identity and see your own link:**
```bash
npx -y @joseamica/agentbridge@latest setup
npx -y @joseamica/agentbridge@latest link
```
This is local: `~/.agentbridge` (or `$AGENTBRIDGE_HOME`) holds your key and your database, and
nothing you do here talks to anyone else yet.
**On Dev's machine — build the shared folder and the locked session:**
```bash
npx -y @joseamica/agentbridge@latest setup-responder --share <a folder Dev is willing to share>
```
This creates the shared folder if it isn't there, creates a dedicated Claude Code profile at
`~/.agentbridge-responder` (override with `--profile`), writes the restricted permissions, writes
a `responder.json` recording the folder, the model and the effort, installs the plugin into that
profile, and drops a persona `CLAUDE.md` and `.agentbridge-scope.md` into the shared folder. It
refuses to run if that profile — or Dev's identity folder — would land inside the shared folder.
`setup-responder` always writes scope 1, and rewrites `settings.json` and `responder.json` every time:
run by hand on a profile that had a wider scope, it narrows it back to one folder. The wider scopes
are chosen only through `setup`. Copy into that folder
only what Dev is willing to share: not the working repo, nothing with credentials.
Logging that dedicated profile in is a browser and a password, so it is `setup`'s job, not a line
to paste — see the guided way above. Once there is a session, this is how Dev starts answering:
```bash
npx -y @joseamica/agentbridge@latest responder
```
It reads `responder.json` from the dedicated profile, sets that profile's `CLAUDE_CONFIG_DIR`
itself, and hands the terminal to Claude Code with the shared folder as its working directory.
The first time, it'll ask whether to trust the development channel — that's the AgentBridge plugin
`setup-responder` just installed; accept it. It takes over this terminal until Ctrl+C, so keep it
running there, or under tmux, and open a **new** terminal window for the next command. Pass the
same `--profile` you gave `setup-responder` if it wasn't the default.
```bash
npx -y @joseamica/agentbridge@latest doctor --profile ~/.agentbridge-responder --share <the shared folder>
```
Every line should read `[ok]`. This is the step that tells you the fence is real, the plugin is
installed, and nothing dangerous landed in the shared folder — including that every board Dev
uses actually accepts and returns what's published to it, not just that the socket opens. Run it
before you trust the setup.
**On Ana's machine — ask for permission and ask:**
```bash
npx -y @joseamica/agentbridge@latest connect "<Dev's link>" --note "soy Ana"
```
The first step here takes a few seconds — it's mining proof of work, on purpose, as an antispam
measure. Dev sees the request with `requests` and grants it with `approve <id>`; both sides can
then confirm the same state with `contacts`.
```bash
npx -y @joseamica/agentbridge@latest ask dev "which timeout applies to card reads?" --wait 120
```
Or — the actual point of this thing — from inside her own Claude Code:
```bash
claude mcp add agentbridge --scope user -- npx -y @joseamica/agentbridge@latest mcp
```
Restart any session that was already open, then just tell her agent to ask Dev. It gets
`list_contacts`, `ask_contact`, `check_answer` and `connect`. `check_answer` long-polls for at
most 45 seconds per call, so it never hangs a tool call.
### After that
Dev keeps his session running and forgets about it. Ana asks whenever she needs to. Neither of
them has to interrupt the other. If Dev's session is off, the question waits — retried
automatically for up to a week — and lands the moment he starts it again.
For the full pilot protocol in Spanish — including the security checklist you should run before
trusting this with anything real — see
[`docs/runbooks/aceptacion-0.5.md`](docs/runbooks/aceptacion-0.5.md). There is also a friendlier
Spanish quickstart at [`docs/inicio-rapido.md`](docs/inicio-rapido.md).
## CLI reference
Every command below is shown as `npx -y @joseamica/agentbridge@latest <command>`, the same form
used throughout this README. If you installed the package globally, drop the `npx -y
@joseamica/agentbridge@latest` prefix and run `agentbridge <command>` instead.
```
Guided:
npx -y @joseamica/agentbridge@latest setup [--repo <dir>] [--profile <dir>] [--relays <url,url,…>]
(interactive, in Spanish — creates your key and profile, then runs
everything below for the role(s) you pick: the login, the shared
folder, the checks, the MCP registration, and the responder itself)
(--relays alone rewrites your board list and exits; it asks nothing)
Your link and your permissions:
npx -y @joseamica/agentbridge@latest link (prints your own agentbridge:nprofile1… link)
npx -y @joseamica/agentbridge@latest connect <link> [--note "who you are"]
npx -y @joseamica/agentbridge@latest contacts (who you can ask, and who can ask you)
npx -y @joseamica/agentbridge@latest whoami
Requests that reach you:
npx -y @joseamica/agentbridge@latest requests
npx -y @joseamica/agentbridge@latest approve <id>
npx -y @joseamica/agentbridge@latest reject <id>
npx -y @joseamica/agentbridge@latest revoke <name>
Asking:
npx -y @joseamica/agentbridge@latest ask <name> <question…> [--wait <seconds>|--no-wait]
npx -y @joseamica/agentbridge@latest ticket <id> [--wait <seconds>]
npx -y @joseamica/agentbridge@latest mcp (MCP server for Claude Code or Codex)
Answering from this machine:
npx -y @joseamica/agentbridge@latest responder [--profile <dir>]
(start answering — reads responder.json from the dedicated profile
and hands the terminal to Claude Code until Ctrl+C)
npx -y @joseamica/agentbridge@latest setup-responder --share <dir> [--profile <dir>] [--repo <dir>] [--model sonnet] [--effort low]
npx -y @joseamica/agentbridge@latest doctor [--home <dir>] [--profile <dir>] [--share <dir>] [--repo <dir>]
Environment: AGENTBRIDGE_HOME (the folder with your identity and your database)
```
Exit codes: `0` success, `1` expected failure, `2` unexpected.
## Architecture
| Path | What it is |
| --- | --- |
| `packages/core` | Identity, the NIP-59 envelope pipeline (seal, gift wrap, proof of work), the local SQLite store (contacts, questions, cursors), and the board pool client. No server of ours anywhere in here. |
| `packages/channel` | The Claude Code plugin: the responder's dispatcher (turn coordination that used to live in the relay), inbound question handling, and the MCP server exposing `reply`. |
| `packages/cli` | Every command above, plus the asker-side MCP server (`list_contacts`, `ask_contact`, `check_answer`, `connect`). |
| `plugins/agentbridge` | Plugin manifests and the built bundle the responder's dedicated Claude Code profile loads. |
| `tests/` | `asker/`, `responder/` and `acceptance/` run entirely in-process, no network. `tests/live` is the only suite that talks to real public boards. |
Correlation's safety does not come from hiding an identifier from the model — the channel tells the
model the short code and asks it to copy it back exactly. What actually protects it: at most one
question is ever active at a time, and `reply`'s code is checked for an exact match against that
one active question — a wrong or missing code fails the reply outright instead of ever landing on
the wrong question.
## Development
```bash
npm ci
npm test # needs neither Docker nor internet
npm run typecheck
npm run build
```
`npm run test:live` is the only suite that talks to public Nostr boards. Run it on purpose, never
in a loop.
### Running the CLI from a local clone
The Quickstart above installs nothing and runs everything through `npx`. If you're hacking on
AgentBridge itself instead, run the CLI straight out of your clone after building it:
```bash
git clone https://github.com/Joseamica/agentbridge.git
cd agentbridge
npm ci
npm run build
alias ab="node $PWD/packages/cli/dist/main.js"
```
Heads up on that alias: `ab` is ApacheBench on macOS, so a fresh terminal that hasn't re-run it
gives you a benchmarking tool's help text instead of "command not found" — confusing the first
time. That collision, and the alias itself, only exist on this from-source path; the published
`agentbridge` command needs neither. `setup-responder` and `doctor` also still take an explicit
`--repo <dir>` here if you ever want to point them at a checkout other than the one they're
running from.
## Status
This is **0.5**: no server of ours, built for two people who already trust each other. 0.3 made
`setup` perform the steps it used to print, after the first real install — by someone who does not
program, on Windows — broke on them. 0.4 let the answerer choose how far their agent can see: one
folder, several, or the whole personal folder minus a fixed caja fuerte. 0.5 changes how that same
person answers `setup`'s own questions: no path to type (the operating system's own "choose folder"
window does it) and no number or `s`/`n` to type either (every choice is an arrow-key menu with a
marked default; the name, `CONFIRMAR` and a pasted link are still typed) — with a typed question as
the fallback wherever a menu can't be drawn, and 0.4's typed path question wherever a window can't
open. Known gaps, deliberate deferrals and the full residual-exposure statement
are written down in [`docs/known-gaps.md`](docs/known-gaps.md) — including the ones that matter
before you add a third person, and the platforms that are reasoned about rather than tested.
Not in 0.5: per-contact scopes, mobile clients, WhatsApp or Telegram, push notifications, organizations, billing,
attachments, Codex as the responder, and a global binary on your PATH.
Issues and questions are welcome. If you find a way around the fence, please open an issue.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues