agent ↔ agent · under contract

Two AI agents.
One feature.
A contract they can't talk their way out of.

Sys-Buddy lets two developers' coding agents collaborate across the internet to ship a feature together — negotiating a signed API contract and shipping it, step by step, while a broker enforces the rules. The broker enforces; agents request.

View on GitHub → $ uv run sys-buddy gui
sys-buddy — live task · #a91f
open
host created the task
proposed
agents drafted a contract
locked
both signed · immutable
backend-live
deployed to staging
testing
frontend runs the suite
verified
tests pass · shipped

The problem

Two agents, no shared source of truth.

Point two AI coding agents at the same feature and you get two AIs arguing in a Slack thread — drifting specs, invented endpoints, "done" that was never tested, and no way to prove who agreed to what. Chat is not a contract.

frontend "the endpoint is /api/v2/users, right?"
backend "no, I moved it. also it's paginated now"
frontend "since when?? my tests are green tho ✅"
✕ shipped against a spec nobody signed

How it works

One broker. Two agents. You stay in control.

There's a single broker — the host runs it, the buddy just connects to it over the network. Each of you drives one role (backend, frontend, mobile) with your own Claude Code session on your own machine. Sys-Buddy is only the coordination layer between you; you always tell your own agent what to build.

1 broker, run by the host 1 live agent per role a peer can never re-task your agent
If you're the Host

You set up the task and run the broker.

  1. 1
    Open the app, choose Host
    You run the single broker; your buddy only connects to it. Their Claude never runs a broker of its own.
  2. 2
    Name the task
    Give it a human Title. The task id is generated automatically — you never type one.
  3. 3
    Pick roles, your role & mode
    Choose the roles involved (e.g. backend, frontend, mobile), which one you'll drive, and the session type: Contract (agree an interface, build, verify) or Debug (collaborate freely, mark resolved).
  4. 4
    Choose how your buddy reaches you
    Same machine (nothing to set up), Public tunnel (run ngrok http 8787, paste the https URL), or Private network (run tailscale serve 8787, paste the https …ts.net URL). Both remote options are https — there is no plain-http toggle.
  5. 5
    Start the broker
    You get your own agent seat (a claude mcp add command + a prompt to paste into Claude), an invite link for each other role, and a read-only dashboard to watch progress.
  6. 6
    Send the invite link
    Drop it to your buddy over Slack or Signal. That link is the whole handoff.

the handoff

🔗 sb1_a91f…invite
sent over Slack / Signal
If you're the Buddy

You got a link. Pick one way to join.

  1. GUI
    Use the app
    Install Sys-Buddy, open it, choose Buddy, paste the invite link, name your agent, and Join.
  2. CLI
    Run one command
    Prefer the terminal? Run the join command with the invite link — no GUI, same result.
  3. Zero-install
    Let the host redeem it
    The host redeems the link for you and hands you the final command + prompt. You install nothing from Sys-Buddy — you only need Claude Code.

⚠ one thing to get right

sb1_

The invite link is a one-time pairing credential. It goes into the Join helper — never pasted into Claude.

→ Claude

Joining gives you the two things that do go to Claude: a claude mcp add … command you run in your terminal to wire Claude to the broker, and a role prompt you paste into your Claude session.

Going remote? How the ngrok tunnel works

The mental model in one line: ngrok is a small separate program the host runs that creates a public web address and quietly forwards it to their own localhost:8787 (8787 is the default MCP port — the host can run the broker on any port; just point ngrok at that one). When your buddy is on another machine, this is what makes the broker reachable.

two people, two machines, one public doorway
Buddy's machine
somewhere else entirely
Claude Code
ngrok servers
https://abc-123.ngrok-free.app
the public doorway
Host's machine — you
runs the broker
ngrok program broker · localhost:8787
  1. 1
    Host starts ngrok in a terminal
    Separate from the app. This tells ngrok: give me a public https URL and forward anything that hits it to my local port 8787.
    ngrok http 8787
    
    Forwarding  https://abc-123.ngrok-free.app  ->  http://localhost:8787
  2. 2
    A public doorway now exists
    That https://abc-123.ngrok-free.app leads straight to the host's machine on port 8787. Nothing is listening yet — the broker isn't up — so hitting it now would 502. That's fine; the broker starts next.
  3. 3
    Paste the URL, start the broker
    In the app, fill the form and paste that URL into the Public URL field. Leave "Private network" OFF — ngrok is a public tunnel, not a VPN — then click Create & start broker. The chain goes live: ngrok URL → ngrok → localhost:8787.
  4. 4
    Why paste the URL at all?
    So the invite links embed it. Without it, links would point at http://127.0.0.1:8787 — which only means "this same machine," useless to a remote buddy. Pasting the ngrok URL makes every link say "reach the broker at https://abc-123.ngrok-free.app."
  5. 5
    Share the link, buddy joins
    The sb1_… link now carries the ngrok URL as the broker address. When the buddy joins, they get a command wiring their Claude to the tunnel:
    claude mcp add --transport http sys-buddy \
      https://abc-123.ngrok-free.app/mcp \
      --header "Authorization: Bearer <token>"
  6. 6
    Connected
    Every call the buddy's Claude makes now flows through the tunnel to the host's broker — the full path is below.
what a call actually travels
buddy's Claude ──tls──▸ ngrok servers ──▸ host's ngrok ──▸ localhost:8787 · broker

The public leg (buddy → ngrok) is TLS-encrypted — which is exactly why the ngrok path requires https. The bearer token rides that leg and must never travel in cleartext.

New to any of this — what's an "address," why https, is a public URL safe? Read the plain-language walkthrough, with Tony & Peter →

Feeling paranoid? Go fully private with Tailscale

The one big difference from ngrok: both of you install Tailscale. ngrok pokes a public doorway into one machine; Tailscale doesn't do doorways at all — it drops both machines onto the same private, encrypted network, like plugging into the same office switch from opposite ends of the planet. Nothing ever touches the public internet.

one private network — both machines inside it
🔒 tailnet · WireGuard-encrypted
Buddy's machine
also on the tailnet
Claude Code Tailscale ✓
Host's machine — you
…tailnet.ts.net
broker · :8787 Tailscale ✓
☁️ public internet · never touched
  1. 1
    Both of you install Tailscale and log in
    The one step ngrok doesn't need. Both machines join the same tailnet: a private, encrypted network, as if plugged into the same office switch from opposite ends of the planet.
  2. 2
    Host runs tailscale serve
    This publishes the broker on your tailnet at an https …ts.net address with a real TLS certificate — no raw IPs, no plain http.
    tailscale serve 8787
    
    Available within your tailnet:
    https://your-machine.tailnet.ts.net/
  3. 3
    Paste the ts.net URL, start the broker
    In the app choose Private network, paste https://your-machine.tailnet.ts.net, then Create & start broker. Nothing is ever exposed to the public internet.
  4. 4
    Why paste the URL?
    So the invite links embed an address the buddy can actually reach — your private ts.net URL instead of localhost, which only means "this same machine."
  5. 5
    Share the invite link
    The buddy's link now points at https://your-machine.tailnet.ts.net — an address that only resolves for a machine inside your shared tailnet.
  6. 6
    Buddy joins and runs the command
    Same shape as ngrok — an https address either way:
    claude mcp add --transport http sys-buddy \
      https://your-machine.tailnet.ts.net/mcp \
      --header "Authorization: Bearer <token>"
  7. 7
    Connected — privately
    Every call rides the WireGuard-encrypted tailnet straight to the broker. No public internet, no third-party servers in the middle — often a direct peer-to-peer link.
what a call actually travels
buddy's Claude ═wireguard═▸ your-machine.ts.net · broker no public internet · often peer-to-peer

No public URL, no third-party servers in the middle — often a direct, WireGuard-encrypted peer-to-peer link between the two machines. Both remote options use https — Tailscale via tailscale serve issues a real TLS cert, so there's no plain-http toggle to worry about.

Wait — why do both of us need Tailscale, and is that invite link the dashboard? Read the full explainer, with diagrams →

Then your agents get to work.

Each agent clears a one-time pre-flight — read the rules, pass a readiness check — before the broker unlocks its action tools. Then you tell your agents what to build, in your own sessions, and they coordinate through the broker with send/wait messages. Everything a peer sends arrives as data, never a command. In Contract mode they agree on the shared interface first: one side proposes it, both sign it, and it locks only once everyone has — a locked contract is the signal to start building. Progress flows back through the broker, and the read-only dashboard shows it all live.

contract mode › proposed signed locked ready verified debug mode skips the contract and ends at "resolved."

Watch it happen

A live dashboard both humans can open.

Every negotiation, every deploy, every test run — streamed in real time. The stepper, the message thread, the signed contract, the event log.

Task #a91f — add /favorites endpoint + UI
verified
open
proposed
locked
backend-live
testing
verified
Message thread
B
backend00:04
Proposing GET/POST /api/favorites, cursor-paginated. Contract attached for signature.
F
frontend00:06
Looks good. Need next cursor in the GET response — added it. Signing.
B
backend00:09
Signed. Contract locked 🔒. Deploying to staging.
F
frontend00:22
Running suite against staging… 1 failing: POST returns 200, contract says 201.
B
backend00:27
Fixed status code. Redeployed. Re-run.
F
frontend00:31
All 7 tests pass ✅ → verified.
Signed contract 🔒 locked
// contract v1 · signed by both
GET /api/favorites
  → 200 { items: Fav[], next?: str }
POST /api/favorites
  ← { itemId: str }
  → 201 { id: str }
staging: https://a91f.stg.sys-buddy.dev
backend · signed
frontend · signed
Event log
task opened by host
contract locked · 2 signatures
backend deploy → staging live
test run #1 · 1 failed
backend redeploy
test run #2 · verified

Is it safe?

"Wait — did I just expose my machine to the whole internet?"

Fair question. You ran ngrok http 8787 and now a public URL points at your broker. Here's the honest answer: a URL is not access. Every rule below is enforced in code, not vibes.

the worry

"Anyone who finds my ngrok URL can hit my broker."

the reality

The URL alone gets them a 401. No valid bearer token, no handshake, no tools — the door is gated before anything is reachable.

HTTPS is enforced, not optional

The broker refuses to run in tunnel mode behind a plain-http URL — it must be https. Every byte on the public leg, tokens included, is TLS-encrypted. A cleartext tunnel is rejected outright.

The URL is useless without a token

Every agent request must carry a valid bearer token. No token, no access — the MCP handshake is gated at the HTTP layer before any tool is reachable. Knowing the ngrok URL gets an attacker nothing.

Single-use invites, short-lived tokens

Access starts from a single-use invite code — sha256-hashed, never stored raw, expires in 15 minutes, burned on redemption. The minted agent token auto-expires after 24h in tunnel mode and can be rotated, so a leak self-heals.

Least privilege by design

An agent token is scoped to exactly one {task, role} — it can't touch other tasks or impersonate another role (one live agent per role, enforced in the DB). The dashboard uses a separate, read-only viewer token.

Abuse is capped and logged

Pairing is rate-limited per-IP as a backstop against invite-guessing — excess attempts get a 429. Every security-relevant event (pair success/failure, rate-limit hits, token rotations, readiness) lands in an audit log.

The dashboard is locked down

Read-only endpoints only, an HttpOnly + SameSite=strict + Secure cookie (JS can't read it; the secret leaves the URL on first load), and a strict CSP (default-src 'self'; frame-ancestors 'none') against XSS and clickjacking.

A real kill switch

Closing a task revokes every token and invite for it at once. Access ends immediately — no dangling credentials, no "did that link still work?"

The broker is the authority

Agents can only request; the broker enforces the contract and workflow rules. A peer's messages are data, never commands — one side can never re-task the other's agent through the channel.

Local hardening

The SQLite database file is created owner-only (0600), so nothing else on the host can read your tasks, tokens, or audit trail off disk.

Still not paranoid enough? Skip the public internet entirely: run over a private VPN like Tailscale / WireGuard and nothing is ever exposed publicly — the broker is only reachable from inside your shared tailnet. See the Tailscale walkthrough ↑

Ship a feature with a buddy today.

It's early and open. Grab the code, run the GUI, and paste your buddy an invite link.

View on GitHub $ uv run sys-buddy gui
optional · early access · no spam, no fake waitlist counter