For agents
An agent is a member of its own. It signs in with its own key, works in its own squares, and every commit it makes says which agent made it and for whom.
Everything here works the same from any AI tool: the CLI, the MCP tools and HTTP all reach the same server operations and answer in the same words.
Sign an agent in
- On heyquilt.com, make an invite for the agent (Agents) and register the agent with it, as the invite says. The agent gets its own keys: an access key (it starts with
qa_), a refresh key and a resume key. Its id is on its row under Agents. - A person who admins the quilt adds the agent as a member:
$ seam quilts add agent:5c1e… --level write --quilt my-app - Give seam the agent's access key with
SEAM_TOKEN:$ export SEAM_TOKEN=qa_…
With SEAM_TOKEN set, every seam command acts as the agent instead of whoever signed in with seam login. SEAM_API_URL points it at another server; the default is https://api.heyquilt.com.
Each commit records the agent as its author, with its name, member id and AI tool, and the person who owns the agent as on_behalf_of. seam log shows it as Author: builder <agent:5c1e…> via claude.
The CLI on a cloud machine
On a server, a container or an agent's sandbox, install with npm install -g @heyquilt/seam, or use npx @heyquilt/seam without installing, with SEAM_TOKEN set. One square per task:
$ seam square open "Rate limit sign-in" --quilt my-app --dir work -- src
$ cd work
# edit files with your own tools
$ seam status
$ seam commit -m "Rate limit the sign-in route"
$ seam merge --dry-run
$ seam merge
Merged as patch main:784 (b7c8d9e0f1a2).- Add
--jsonto any command for the server's full record instead of text. - Check
seam events(orseam square show) between steps. When it says a patch would conflict, runseam rebaseand settle it in your square before you merge. seam commitworks only in a block folder. To commit without a folder, useseam_commitwith file contents, or the HTTP API.
MCP tools
seam mcp serves the same commands as MCP tools over stdio, for any AI tool that speaks MCP: the same tools, descriptions and answers. Add it to your tool as a server named seam that runs seam mcp (with SEAM_TOKEN in its environment for an agent).
{ "mcpServers": { "seam": { "command": "seam", "args": ["mcp"] } } }- In a block folder (one with
.seam/block.json), the quilt and square come from the folder, andseam_commitseals the folder's changed files. Edit with your own tools. - Anywhere else, name the
quiltandsquarein each call, and passseam_committhe new contents:files: [{ path, content }](base64: truefor bytes) anddelete: [paths].
| Tool | What it does |
|---|---|
seam_quilts | Lists the quilts you can reach, or makes a new one (new: its slug). |
seam_open_square | Opens a square from the tip of main (or line) and returns its sq: id. |
seam_squares | The open squares, with holder, head, how far behind main and conflicted paths. all: true for every square. |
seam_log | History, newest first: rev, paths, limit (default 20). |
seam_show | One commit or patch: who, when, why, which square, and its diff. |
seam_diff | The difference between two revisions (from, to). |
seam_blame | Each line of a file with the commit and patch that last changed it. |
seam_cat | A file at any revision; a conflicted file comes back with its markers (format diff, snapshot or git). |
seam_ls | The files at a revision, under paths. |
seam_commit | Seals your square's changes as a commit: the block folder's changed files, or files: [{ path, content }] and delete. |
seam_merge | Merges your square into a line (into: "main"), or another square or revision into yours (from). dry_run says what would happen. |
seam_rebase | Moves your square onto the tip of its line (or onto). Conflicts become markers, never a stop. |
seam_revert | Opens a new square whose one commit undoes a patch or commit. |
seam_cherry_pick | Applies one commit's change (from) to your square as a new commit. |
seam_resolve | Settles conflicted files by choosing a side: ours, theirs, base, suggestion or delete. |
seam_square_status | One square as the server sees it: head, behind_by, conflicts, what merged under it, what would conflict. |
seam_square_handoff | Hands your square to another member (to) with a note on what is done and what is left. |
seam_abandon | Abandons a square whose work is not wanted. Its commits stay readable. |
seam_reopen | Reopens an abandoned square; you become its holder. |
seam_fold | Folds a square into another square you hold (into) as one commit, and closes it. |
seam_split | Splits your square by paths: each group moves to a new square. |
seam_events | Your events since the cursor after: patch.merged, conflict.added, square.handoff. |
seam_events_webhook | Has the server POST your events to a URL, signed; url: "off" removes it. |
Revisions are the same as everywhere: main, main~3, patch:12, sq:<id>, a commit id or git:<sha>.
An agent that only speaks HTTP (no command line, no MCP) uses the HTTP API with its access key.
Habits that keep work safe
- Read a file from your own square (
rev: sq:<id>), not frommain.mainmoves while you work, and a commit built from a newermainthan your square started from can undo work that merged in between. - When a write answers that the square moved on since you looked, look again with
seam_square_statusand retry. - A refused merge is not a failure: the conflict is in your square. Settle it and merge again.
Events and webhooks
Three kinds of event reach a member:
| Event | Sent when | To |
|---|---|---|
patch.merged | A patch merged that changed a file your open square on that line also changed. Says whether your merge would now conflict. | The square's holder |
conflict.added | A commit wrote a new conflict into your square. | The square's holder |
square.handoff | A square was handed from one member to another, with the note. | Both of them |
Read them
$ seam events --quilt my-app
#41 patch.merged: patch 784 by person:7f3e… "Fix sign-in" changed src/auth.js in your square sq:77aa01bc; would conflict in src/auth.js
(next: after=41)
$ seam events --quilt my-app --after 41 --kinds patch.merged,conflict.addedPass the cursor from the last answer as --after to get only new ones. Over HTTP: GET /v1/quilts/<q>/events?after=41.
Be woken by a webhook
$ seam events --quilt my-app --webhook https://agent.example.com/quilt
Events (patch.merged, conflict.added, square.handoff) now POST to https://agent.example.com/quilt
Signing secret (shown once): …
$ seam events --quilt my-app --webhook off
Webhook removed.- One webhook per member per quilt. The URL must use
https. --kindspicks the events (all three by default).--secretsets your own signing secret, 16 to 200 characters; otherwise one is made and shown once.- Each POST is JSON with the headers
x-quilt-event,x-quilt-delivery,x-quilt-timestampandx-quilt-signature. - A failed POST is tried again after 1, 5 and 25 seconds. Answer with a 2xx quickly.
{
"event": "patch.merged",
"id": "019a1f2c-…:41",
"to": "agent:5c1e…",
"ts": 1761400000000,
"kind": "patch.merged",
"event_seq": 41,
"at": 1761400000000,
"cause_seq": 5120,
"quilt": "019a1f2c-…",
"square": "019a2b7e-…-0d3e77aa01bc",
"square_short": "sq:77aa01bc",
"line": "main",
"patch_no": 784,
"by": { "member_id": "person:7f3e…", "kind": "person", "name": "Ada" },
"paths": ["src/auth.js"],
"payload": {
"patch_no": 784,
"commit": "5f4e3d2c1b0a…",
"message": "Fix sign-in",
"author": "person:7f3e…",
"stat": [{ "path": "src/auth.js", "op": "put" }],
"would_conflict": true,
"would_conflict_paths": ["src/auth.js"]
}
}Check the signature before you trust a POST. It is sha256= and the HMAC-SHA256 of <timestamp>.<body> with your secret:
import crypto from 'node:crypto'
function fromQuilt (secret, req, body) {
const ts = req.headers['x-quilt-timestamp']
const want = 'sha256=' + crypto.createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex')
const got = String(req.headers['x-quilt-signature'] || '')
return got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))
}