skip to content
agentmash get started
docsthe public api

run it yourself

the public api

npx agentmash login connects the coding agents it knows, and there is always one it does not: the one your team built itself, a bot, a CI job, an editor plugin. This page is for that one. There are two ways in, and they reach the same room.

  • Your tool has hooks (a command it runs before and after a tool call, with JSON on stdin): register the generic hook, agent_hook.mjs, and it answers in the format your tool reads. It reports the edits, asks before one and words the advice exactly as the Claude Code hooks do. See your own tool's hooks below.
  • Your tool talks HTTP: call /api/v1, described here, with a room key or a personal API token. The machine-readable version is GET /api/v1/openapi.json (OpenAPI 3.1).

The wire contract under both is PROTOCOL.md. /api/v1 is protocol 1 under a versioned path, plus what a client of its own needs: a bearer token, the advice an agent should be shown, a stream it can read without a WebSocket, and errors in one shape.

Base URL and versioning

https://agentmash.dev/api/v1, or /api/v1 on your own server. Every route of protocol 1 is there under the same name (POST /api/v1/events is POST /events, answered by the same code), and the unversioned routes stay where they are, unchanged, for the hooks already committed to repositories: they are never removed.

Within v1, changes are additive only: new optional fields, query parameters, endpoints, frame types and error codes. Ignore a field, a frame type or a code you do not know, and fall back on the HTTP status. A change that would break a client goes to /api/v2, which runs beside v1 for at least twelve months; a v1 route on its way out sends Deprecation and Sunset headers at least six months before it goes, and this page says so.

Authentication

Send one credential, as Authorization: Bearer <token>. The older headers work too, and mean the same:

TokenWhat it isOpensThe same as
mash_…the room key, from .claude/agentmash/config.jsonits own roomx-agentmash-room: mash_…
amu_…a machine's sign-in, from npx agentmash loginevery connected repository its login can push tox-agentmash-user: amu_…
amp_…a personal API token, from agentmash.dev/keys or npx agentmash token createthe same, held to its scopes and repositories(v1 only)
  • A room key names its own room. With an amu_ or amp_ token, name the room by its ref: x-agentmash-room: rm_…, or ?room=rm_…, which is safe because a ref is not a secret. GET /api/v1/lookup?repo=owner/name gives the ref for a repository. A room-scoped route with no room named answers 400 room_required.
  • A developer token (amt_…, from a room whose token_mode is on) still goes in x-agentmash-token beside the key; sent as a Bearer it answers 400 invalid_credentials. Two credentials that disagree answer 400 ambiguous_credentials; anything else as a Bearer answers 401 invalid_token, with a WWW-Authenticate header.
  • A room key in a repository that is public on GitHub, whose room is connected to it, is anyone's who reads it, so it opens the room only beside the sign-in of someone who can push there: send x-agentmash-user: amu_… with it. Alone, or beside anyone else's sign-in, it answers 403 room_key_public, with the repository as repo.
  • A room key in a query string (?room=mash_…) answers 400 key_in_url everywhere but /api/v1/live and /api/v1/stream, because a browser cannot set a header on a WebSocket or an EventSource. Anywhere else it would land in every access log between you and us.
  • /api/v1 reads no cookie, so a page open in someone's browser cannot call it as them.

Which one to use. A room key is a shared secret with no expiry, for one room: right for CI and bots. A personal token is a person's, for their own tools: it opens what its login can push to, and like every sign-in it goes quiet when that person has not signed in for seven days (AGENTMASH_PUSH_ACCESS_DAYS), answering 401 reauth until they run npx agentmash login or sign in on the website again. A room connected to a repository that is public on GitHub has no credential that lasts: its keys open it only beside such a sign-in (room_key_public, above), which goes quiet the same way, so CI there has to sign in again at least once a week.

Personal API tokens

The quickest way is agentmash.dev/keys: sign in with GitHub, make a key, copy it, and the page shows the first calls to paste, with your room's ref in them. A signed-in machine makes them too; nothing else can, a room key and another token included (403 machine_token_required).

bash
npx agentmash token create --name ci-bot --read-only --repo acme/app --expires 30d
npx agentmash token list
npx agentmash token revoke ci-bot

create prints the token alone on stdout, so TOKEN=$(npx agentmash token create --name ci) keeps it, and says the rest on stderr. The token is shown that once and kept only as a hash.

  • Scopes: read and write by default; --read-only gives a token that answers 403 insufficient_scope to anything but a GET.
  • Repositories: every one its login can push to by default; --repo owner/name, once or more, limits it, and a room of another repository answers 403 token_repo_forbidden.
  • Expiry: 90 days by default, 1 to 365 (--expires 30d), or --expires never, only when asked for by name.
  • Limits: 25 live tokens a person, 10 made an hour.
  • Where they work: /api/v1 only. An amp_ token on an unversioned route answers 401 api_token_v1_only, so its scopes, repositories and rate limits are checked in one place.
  • Revoking one ends its open live views and streams at once.

Over HTTP, with the machine's sign-in as the Bearer: POST /api/v1/tokens with { "name", "scopes", "repos", "expires_in_days" }, GET /api/v1/tokens and DELETE /api/v1/tokens/{id}.

Endpoints

Method and pathWhat it does
POST /api/v1/eventsReport an edit, a prompt or the end of a session (protocol 1's body)
GET /api/v1/eventsThe room's events, newest first, a page at a time (next_cursor)
GET /api/v1/check?file_path=…Ask before editing a file: who else is in it, and the advice to show the agent
GET /api/v1/claimsLive claims, or those on the given paths or symbols
POST /api/v1/claimsClaim files for a task, for a bounded time; nothing is locked
POST /api/v1/claims/releaseRelease one claim, or every claim of a session
GET /api/v1/collisions?advisory=1Files the caller changed that a teammate changed too
POST /api/v1/resolutionsSay how a collision was settled
GET /api/v1/activityThe room right now: sessions, presence, claims, hot files, recent events
GET /api/v1/teamTeammates at work now, and what each is on
GET /api/v1/liveThe room's frames, live, over a WebSocket
GET /api/v1/streamThe same frames as server-sent events
GET /api/v1/lookup?repo=owner/nameWhich room a repository reports to, for a sign-in or a token
GET /api/v1/roomThe room, for any credential that opens it
GET /api/v1/meWho this credential is
POST /api/v1/tokens, GET /api/v1/tokens, DELETE /api/v1/tokens/{id}Personal API tokens
GET /api/v1/openapi.jsonThis API as an OpenAPI 3.1 document, open to anyone

Room creation, the admin routes (rotation, settings, purge, deletion) and the sign-in flows are not in v1: they stay on the routes the CLI calls, in the README's API section.

Asking before an edit

GET /api/v1/check is /check with one more field, advice: the exact text an agent should read before it edits the file, worded by the same code the hooks use, so your tool says what Claude Code is told, and says it differently only when the hooks do.

bash
curl -s "https://agentmash.dev/api/v1/check?file_path=src/models/user.ts&developer=bob&branch=main" \
  -H "Authorization: Bearer $AGENTMASH_ROOM"
json
{
  "conflict": true,
  "holders": [{ "developer": "alice", "file_path": "src/models/user.ts", "minutes_ago": 2, "branch": "main", "task_hint": "add soft deletes", "change_summary": "+12/-3 lines, touching deletedAt" }],
  "claimed": false,
  "claims": [],
  "advice": {
    "level": "active",
    "severity": 4,
    "developer": "alice",
    "file": "src/models/user.ts",
    "text": "[AgentMash: ACTIVE COLLISION] alice's agent is working in src/models/user.ts — last write 2 min ago on main, as you are, while working on 'add soft deletes' (+12/-3 lines, touching deletedAt). …",
    "parts": [{ "level": "active", "developer": "alice", "text": "…" }],
    "strict_block": true
  }
}

Show advice.text as it is. level says how loud it is, from nearby to active, and claimed for a claim alone; severity is the same as a number, -1 to 4. strict_block is whether a client in strict mode stops the edit: a claim alone never does. advice is null when there is nothing to say, and null with advice_unavailable: true on the rare answer the server could not word. branch and checkout_id make the wording right for a teammate on another branch, or in another clone of yours; hints=0 leaves teammates' task descriptions out.

Reporting an edit

POST /api/v1/events takes protocol 1's event, one per file, with the path relative to the repository root and tool naming your client:

json
{
  "developer": "bob@example.com",
  "session_id": "your-tool-session-42",
  "event_type": "edit",
  "tool": "mytool:Edit",
  "file_path": "src/models/user.ts",
  "git_branch": "main",
  "task_hint": "add a role to users",
  "change_summary": "+2/-1 lines, touching role"
}

What each field means is PROTOCOL.md, and what a client must do when the server is slow or down is §6: a hard timeout, no retries, and on any failure, nothing at all.

Live: a WebSocket, or server-sent events

GET /api/v1/live upgrades to a WebSocket; GET /api/v1/stream sends the same frames as text/event-stream, for a client that would rather read lines. Each frame is a JSON object with a type: hello, event (with the changed text when the room streams diffs), claim, claim_released, claims_released and resolution; ignore a type you do not know.

On the stream, each frame is event: <type> and data: <the same JSON>, with id: on event frames. It opens with retry: 5000, sends a : keepalive comment every 25 seconds, and ends with an event: close frame carrying the code: 4001 when the credential no longer opens the room (stop reconnecting), 4003 when the room was deleted, 1001 when the server is restarting. A credential holds five streams at once and a server a thousand; past that, 429 stream_limit. There is no replay from Last-Event-ID yet: a client that reconnects reads GET /api/v1/events for what it missed.

Errors

Every error on /api/v1 has one shape, and every answer an x-request-id header:

json
{ "error": "a sentence for a person", "code": "room_required", "request_id": "6f0c…" }

code is stable and snake_case; some errors carry more fields (a claim's outcome, a fork's fork). Quote request_id when you write to us: it is the one thing in our log that finds your request, which names a route and never its address, a header or a body.

StatusCodes
400room_required, key_in_url, ambiguous_credentials, invalid_credentials, file_path_not_relative, invalid_request
401room_unauthorized, reauth, signed_out, invalid_token, token_required, api_token_v1_only
403insufficient_scope, token_repo_forbidden, machine_token_required, token_unverified, closed, room_key_public, forbidden
404not_connected, not_found
409token_limit
410room_deleted
413, 415the body is too large; send JSON (json_only)
429rate_limited, stream_limit, with Retry-After

Rate limits

Counted per credential on /api/v1, in a fixed one-minute window: 1200 calls a minute to /api/v1/check, 600 to every other read and 600 writes, which is room for a fifty-file commit reported at once. Each answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, and a 429 a Retry-After. The unversioned routes the hooks call are not limited.

CORS

Any origin may call /api/v1 (Access-Control-Allow-Origin: *), which is safe because it reads no cookie: a page gets in only with a token it holds. The allowed headers are content-type, authorization, x-agentmash-room, x-agentmash-token, x-agentmash-user and idempotency-key; the exposed ones x-request-id, retry-after, ratelimit-limit, ratelimit-remaining, ratelimit-reset and idempotent-replayed; a preflight is cached for 600 seconds. An Idempotency-Key is allowed through but not acted on yet: a write sent twice is recorded twice, as protocol 1 says.

Your own tool's hooks: the generic hook

A tool that runs a command before and after its tool calls needs no client of its own: npx agentmash login puts agent_hook.mjs in ~/.agentmash/hooks, and that one script is what every connector since the first four runs. It reads what your tool sends, asks the room or reports to it through the same scripts Claude Code runs, and answers in your tool's format. The advice, strict mode, claims, symbols and identity are those scripts', so they never drift.

npx agentmash hook --print-command prints the line to register, one per event:

bash
npx agentmash hook --print-command --client mytool --event pre_edit --dialect json
# node "/home/you/.agentmash/hooks/agent_hook.mjs" pre_edit --global --client mytool --dialect json

Register that node line, never npx: npx looks the package up on every call, which takes seconds when it is cold, and the hook before an edit has 1.5 seconds to answer. The line is the same string every time for the same arguments, with no version in it, so an agent that asks the person to approve a hook (Codex does) asks once.

EventWhen your tool runs itWhat it does
pre_editbefore a tool that changes filesasks the room, and answers with the advice, if any
post_editafter onereports each file, in the background, and answers at once
promptwhen the person sends a promptreports it, and answers with a heads-up when a teammate is near
session_startwhen a session startsanswers with that heads-up
stopwhen the agent finishes a turnanswers with a word for the person when a teammate changed the same files
session_endwhen the session endsreports it

--client names your tool, a short lowercase name: the room shows its edits as mytool:Edit and mytool:Write. --global finds the room for the checkout by this machine's sign-in, as every connector's hooks do. To try a configuration by hand, npx agentmash hook pre_edit --client mytool --dialect json < payload.json runs the same hook once.

What it reads

With --input native, a dialect reads your tool's own JSON (the connectors' dialects each read one agent's). Otherwise, send this, and it is read whatever your tool is:

json
{
  "agentmash": 1,
  "session_id": "your-tool-session-42",
  "cwd": "/home/you/src/app",
  "files": [{ "path": "src/models/user.ts", "before": "id: string", "after": "id: string; role: string" }],
  "tool": "replace_in_file"
}
  • session_id is your tool's own stable id for the session, at most 200 characters; cwd is where it works, and the repository is found from there.
  • files, for pre_edit and post_edit, are the files the call changes, absolute or relative to cwd. before and after are the text replaced and its replacement; after alone is a whole new file; edits: [{ "before", "after" }] is several edits of one file. A plain string is a path alone. A file outside the repository is dropped before anything is sent.
  • patch, in place of files, is an apply_patch-format patch; command, after a shell command, has the working tree say what it changed.
  • prompt is what the person typed, for prompt. agent_id and agent_type name a subagent; end_reason goes with session_end, and stop_hook_active with stop.

Who the developer is never comes from stdin: it is the GitHub login the machine signed in with, else their git email, as for every hook.

What it answers

--dialect picks the answer's format. Left out, it is the dialect named like the client when there is one, else claude.

DialectForBefore an edit
claudetools that read Claude Code's hook outputhookSpecificOutput.additionalContext, the advice beside the edit
deny-oncetools whose hook before a tool can only let it run or refuse it, with a reason the model readsthe first try refused, with the advice as the reason; the same edit again goes through
plaintools that put a hook's stdout in front of the modelthe advice as text
jsonanything else, and your own codethe result, below
json
{ "agentmash": 1, "event": "pre_edit", "action": "advise", "level": "active", "text": "[AgentMash: …] …", "files": ["/home/you/src/app/src/models/user.ts"], "strict": false }

action is allow, advise or deny, and deny only in a room's strict mode. A dialect is one file in ~/.agentmash/hooks/dialects/, export default { name, parse, answer, neutral, tools }; the ones the connectors use are there too, and the comment at the top of hooks/agent_hook.mjs is the whole contract.

It fails open

Anything going wrong (an event or dialect it does not know, no --client, stdin that is not JSON, a room that is down or slow, AGENTMASH_DISABLE=1) is the dialect's neutral answer and exit 0, in time: the call goes on as it would have without the hook. Nothing it says ever approves something the person would otherwise be asked about.

from docs/API.md, “The public API”