Source

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

  1. On heyquilt.com, make an invite link for the agent (Agents).
  2. Join with it on the machine the agent runs on. The keys are saved in ~/.quilt/agents/<name>.json, readable only by you. --here keeps them in the project's .quilt/ folder instead, for a sandbox that wipes its home folder.
    $ quilt agent join <invite link> --name builder
    $ quilt agent whoami --name builder
    builder (claude, …): your personal agent
    Agent id: 5c1e…
  3. A person who admins the quilt adds the agent as a member:
    $ quilt quilts add agent:5c1e… --level write --quilt my-app
  4. Point the CLI at the agent's access key with QUILT_TOKEN:
    $ export QUILT_TOKEN=$(jq -r .accessKey ~/.quilt/agents/builder.json)
    Without jq, copy the accessKey value from that file.

With QUILT_TOKEN set, every quilt source control command acts as the agent instead of the account this computer is signed in to. QUILT_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. quilt 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 curl -fsSL https://heyquilt.com/install.sh | sh or npm install -g heyquilt, or use npx heyquilt without installing. One square per task:

$ quilt square open "Rate limit sign-in" --quilt my-app --dir work -- src
$ cd work
# edit files with your own tools
$ quilt status
$ quilt commit -m "Rate limit the sign-in route"
$ quilt merge --dry-run
$ quilt merge
Merged as patch main:784 (b7c8d9e0f1a2).
  • Add --json to any command for the server's full record instead of text.
  • Check quilt events (or quilt square show) between steps. When it says a patch would conflict, run quilt rebase and settle it in your square before you merge.
  • quilt commit works only in a block folder. To commit without a folder, use quilt_commit with file contents, or the HTTP API.

MCP tools

Quilt's MCP server (quilt mcp) offers the source control tools when QUILT_SOURCE=1 is set in its environment. Every AI tool that speaks MCP gets the same tools, descriptions and answers.

  • In a block folder (one with .quilt/block.json), the quilt and square come from the folder, and quilt_commit seals the folder's changed files. Edit with your own tools. Quilt's live-session tools (quilt_status, quilt_before_edit and the rest) refuse there; use the source control tools.
  • Anywhere else, name the quilt and square in each call, and pass quilt_commit the new contents: files: [{ path, content }] (base64: true for bytes) and delete: [paths].
ToolWhat it does
quilt_quiltsLists the quilts you can reach, or makes a new one (new: its slug).
quilt_open_squareOpens a square from the tip of main (or line) and returns its sq: id.
quilt_squaresThe open squares, with holder, head, how far behind main and conflicted paths. all: true for every square.
quilt_logHistory, newest first: rev, paths, limit (default 20).
quilt_showOne commit or patch: who, when, why, which square, and its diff.
quilt_diffThe difference between two revisions (from, to).
quilt_blameEach line of a file with the commit and patch that last changed it.
quilt_catA file at any revision; a conflicted file comes back with its markers (format diff, snapshot or git).
quilt_lsThe files at a revision, under paths.
quilt_commitSeals your square's changes as a commit: the block folder's changed files, or files: [{ path, content }] and delete.
quilt_mergeMerges your square into a line (into: "main"), or another square or revision into yours (from). dry_run says what would happen.
quilt_rebaseMoves your square onto the tip of its line (or onto). Conflicts become markers, never a stop.
quilt_revertOpens a new square whose one commit undoes a patch or commit.
quilt_cherry_pickApplies one commit's change (from) to your square as a new commit.
quilt_resolveSettles conflicted files by choosing a side: ours, theirs, base, suggestion or delete.
quilt_square_statusOne square as the server sees it: head, behind_by, conflicts, what merged under it, what would conflict.
quilt_square_handoffHands your square to another member (to) with a note on what is done and what is left.
quilt_abandonAbandons a square whose work is not wanted. Its commits stay readable.
quilt_reopenReopens an abandoned square; you become its holder.
quilt_foldFolds a square into another square you hold (into) as one commit, and closes it.
quilt_splitSplits your square by paths: each group moves to a new square.
quilt_eventsYour events since the cursor after: patch.merged, conflict.added, square.handoff.
quilt_events_webhookHas 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>.

Habits that keep work safe

  • Read a file from your own square (rev: sq:<id>), not from main. main moves while you work, and a commit built from a newer main than 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 quilt_square_status and 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:

EventSent whenTo
patch.mergedA 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.addedA commit wrote a new conflict into your square.The square's holder
square.handoffA square was handed from one member to another, with the note.Both of them

Read them

$ quilt 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)
$ quilt events --quilt my-app --after 41 --kinds patch.merged,conflict.added

Pass 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

$ quilt 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): …
$ quilt events --quilt my-app --webhook off
Webhook removed.
  • One webhook per member per quilt. The URL must use https.
  • --kinds picks the events (all three by default). --secret sets 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-timestamp and x-quilt-signature.
  • A failed POST is tried again after 1, 5 and 25 seconds. Answer with a 2xx quickly.
A webhook body
{
  "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:

Node.js
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))
}