{
  "markdown": "# agentsync-remote\n\n_Part of the [xylem](https://github.com/jarmstrong158/xylem) stack._\n\nA Cloudflare Worker MCP server that makes **claude.ai on your phone** a peer in\nthe [agentsync](https://github.com/jarmstrong158/agentsync) coordination mesh —\nthe same `claims.json`, the same `agentsync` branch, the same overlap and\ncompare-and-swap rules as local agentsync, with **no git and no local clone**.\n\n[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/jarmstrong158/agentsync-remote)\n\nYour laptop's Claude Code (local agentsync) and your phone's claude.ai\n(agentsync-remote) claim work against the **one** shared `claims.json`. Two\ntransports, one mesh. A local peer and this remote peer are indistinguishable in\n`claims.json` except by their agent id.\n\n> **Sibling, not a fork.** This is a second *transport* onto the same\n> coordination file as local [agentsync](https://github.com/jarmstrong158/agentsync)\n> — not a variant of it. See [`DESIGN.md`](./DESIGN.md) for how `git push`-as-CAS\n> becomes GitHub-contents-API-as-CAS.\n\n## What it gives you\n\nSeven tools over Streamable HTTP MCP:\n\n| Tool              | What it does                                                            |\n| ----------------- | ---------------------------------------------------------------------- |\n| `survey`          | The whole board: every peer's claim, your conflicts, the mailbox.      |\n| `claim`           | Claim work; blocked if it overlaps an active peer (CAS-safe).          |\n| `check_conflicts` | Re-check your claim against peers, optionally on one branch.           |\n| `update_status`   | Move your claim through `planning` / `in-progress` / `done`.           |\n| `release`         | Mark your claim done (done never blocks peers).                        |\n| `history`         | Recent commits on the coordination branch (local + remote interleaved).|\n| `mailbox`         | Human-in-the-loop notes: ask from the desktop, answer from the phone.  |\n\n## Deploy your own (one click, no command line)\n\nSelf-host this Worker in **your own** Cloudflare account. Every step is a click\npath — you never touch a terminal.\n\n### 1. Click **Deploy to Cloudflare**\n\nClick the button above. Cloudflare will:\n\n- fork `agentsync-remote` into **your** GitHub account,\n- create the Worker in **your** Cloudflare account, and\n- connect **Workers Builds** so future pushes to your fork redeploy automatically.\n\nThere are no databases or other resources to provision — this Worker keeps no\nstate of its own (everything lives in your GitHub repo), so the deploy is just\nthe Worker itself.\n\nDuring the deploy dialog Cloudflare shows the Worker's **variables**. Set:\n\n| Variable      | Set it to                                                              |\n| ------------- | --------------------------------------------------------------------- |\n| `REPO`        | **Your** coordination repo, as `owner/name` (the repo whose `agentsync` branch will hold `claims.json`). This is the one you must change. |\n| `AGENT_ID`    | Leave as `jonny-mobile`, or pick an id for this peer.                  |\n| `BRANCH`      | Leave as `agentsync` unless you want a different coordination branch.  |\n| `CLAIMS_PATH` | Leave as `claims.json`.                                                |\n\nFinish the deploy. (If you skipped setting `REPO` here, you can set it later in\nthe dashboard — see below.)\n\n### 2. Make a GitHub token, then add the two Worker secrets\n\nThe Worker reads two **secrets**. These are not part of the deploy dialog — you\nadd them once in the dashboard after the first deploy.\n\n**First, make the GitHub token** (this is the `GH_PAT` value):\n\nGitHub → **Settings → Developer settings → Fine-grained personal access\ntokens → Generate new token**:\n\n- **Repository access:** *Only select repositories* → pick **only** your\n  coordination repo (the one you put in `REPO`).\n- **Permissions → Repository permissions → Contents:** **Read and write**.\n- Nothing else. Generate it and copy the token.\n\n**Then add both secrets to the Worker:**\n\nCloudflare dashboard → **Workers & Pages → your Worker → Settings → Variables\nand Secrets** → **Add** → type **Secret** → add each, then **Deploy**:\n\n| Secret       | Value                                                                              |\n| ------------ | ---------------------------------------------------------------------------------- |\n| `AUTH_TOKEN` | A long random string you invent. It locks the endpoint — treat it like a password. |\n| `GH_PAT`     | The fine-grained GitHub token you just made.                                       |\n\nWhile you're on this screen, confirm the **`REPO`** variable points at your\ncoordination repo (set it here if you skipped it in the deploy dialog).\n\n> Until `AUTH_TOKEN` is set the Worker answers **every** request with `404` (it\n> fails closed). Until `GH_PAT` is set the tools return a clear error naming the\n> missing secret. This is by design — an unconfigured endpoint looks like it\n> doesn't exist.\n\n### 3. Add the connector in claude.ai\n\nFind your Worker's URL: **Workers & Pages → your Worker** shows it, in the form\n`https://<your-worker-name>.<your-subdomain>.workers.dev`.\n\nclaude.ai (web) → **Settings → Connectors → Add custom connector**. Paste your\nWorker URL with `/mcp/` and your `AUTH_TOKEN` appended:\n\n```\nhttps://<your-worker-name>.<your-subdomain>.workers.dev/mcp/<AUTH_TOKEN>\n```\n\nThe token in the path *is* the auth — there is no separate login.\n\n### 4. Test it\n\nAsk Claude: **\"call survey\"**. You should get the coordination board back (empty\n`claims` on a fresh mesh — the Worker bootstraps the branch and file for you, so\na brand-new empty repo needs no manual setup).\n\n## 🔒 Security — the connector URL is a credential\n\nThe connector URL embeds your `AUTH_TOKEN` in the path\n(`…/mcp/<AUTH_TOKEN>`). **Anyone who has that URL can call your Worker and\nread/write your coordination file.** Treat the whole URL like a password:\n\n- Don't paste it into screenshots, issues, chats, or commits.\n- Anyone with the URL is a peer in your mesh — share it only with agents/people\n  you trust.\n- **Rotating the token invalidates old URLs.** To revoke access, change\n  `AUTH_TOKEN` in **Settings → Variables and Secrets** and redeploy; every old\n  `…/mcp/<old-token>` URL immediately returns `404`. Update the connector in\n  claude.ai with the new URL.\n\n`GH_PAT` is likewise a credential — scope it to *only* your coordination repo\nwith *only* Contents: Read and write, so a leak can't reach anything else.\n\n### Why the token is in the URL path, and what that costs you\n\nThis is a **deliberate design choice, not an oversight**. claude.ai custom\nconnectors do not reliably send custom headers, so an `Authorization:` header —\nthe obvious alternative — cannot be depended on. Putting the credential in the\npath is what makes the connector work at all.\n\nBe clear about the price, because it is not the same as a header:\n\n- **URLs get recorded in places request bodies never do.** Browser history,\n  shell history, proxy and CDN access logs, crash reports, bug reports,\n  screenshots, \"copy link\" buttons, and **agent session transcripts**. During\n  the audit that produced this section, the connector URLs for these Workers\n  were found in **~54 occurrences across 13 local session transcripts** on a\n  single machine — none pasted deliberately; they were simply part of the tool\n  configuration an agent echoed back.\n- **The Worker itself does not log it.** Every log line records the route as\n  `/mcp/***`. The leak surface is everything *around* the Worker, which is\n  exactly what you cannot audit.\n- **A leaked token makes the holder a full peer in your mesh** — able to claim,\n  force-claim over you, release your claims, and post to the mailbox — with no\n  second factor and no per-caller identity. There is nothing to revoke except\n  the token, and no log that will tell you who used it.\n\n**Practical guidance:**\n\n1. **Rotate on a schedule**, not just on suspicion — assume the URL has been\n   recorded somewhere you don't control. Rotation is cheap: change `AUTH_TOKEN`,\n   update the connector.\n2. **Rotate immediately** if you've shared a terminal recording, a transcript,\n   a screen capture, or a bug report from a machine where the connector is\n   configured.\n3. Use a **long random token** (32+ bytes, e.g. `openssl rand -hex 32`). The\n   comparison is constant-time in both content *and* length, so length is not\n   observable — but entropy is still your only defence against guessing.\n4. If you ever get the chance to use a header or OAuth instead, **take it**.\n   This tradeoff is forced by the client, not preferred.\n\n## Cross-transport walkthrough — two transports, one mesh\n\nThis is the whole point. The laptop and the phone coordinate through one file.\n\n**On the PC (local agentsync, Claude Code):**\n\n```\n> agentsync claim --task \"refactor auth\" --touches src/auth --branch feature/auth\n{\"status\": \"claimed\", ...}          # writes claims[\"laptop\"] on the agentsync branch\n```\n\n**On the phone (claude.ai + agentsync-remote):**\n\n```\nYou: call survey\nClaude: laptop is active — task \"refactor auth\", touches src/auth, branch feature/auth.\n\nYou: claim task \"tidy auth helpers\", touches src/auth/helpers.ts\nClaude: blocked. Overlap with an active peer claim:\n        laptop — \"refactor auth\" — shared_files: [\"src/auth/helpers.ts\"]\n        Narrow `touches`, wait, or re-call with force=true.\n\nYou: mailbox \"Taking src/auth/helpers.ts once you land the refactor — ok?\" to \"laptop\"\nClaude: posted.\n```\n\n**Back on the PC**, the desktop agent (or you) reads the note via `survey` /\n`mailbox`, answers it, and releases:\n\n```\n> agentsync mailbox \"go for it, helpers are stable now\"\n> agentsync release --note \"refactor landed\"\n{\"status\": \"released\"}              # laptop's claim -> status \"done\"\n```\n\n> ### ⚠️ Known gap: the mailbox is currently WRITE-ONLY across tiers\n>\n> **The \"Back on the PC\" step above does not work yet.** This Worker's\n> `mailbox()` writes a top-level `notes[]` array into `claims.json`, and the\n> **local Python agentsync server has no concept of `notes`** — it exposes no\n> `mailbox` tool, and its `survey()` returns only `me`, `branch`, `partners`\n> and `stale_claims`. A note posted from the phone lands in the file correctly\n> and is simply never surfaced to a desktop peer.\n>\n> That matters more than a missing feature normally would, because the\n> repository-level agent instructions tell agents to raise judgment calls\n> *through this mailbox*. An escape hatch that silently swallows the question\n> is worse than no escape hatch: the agent believes it has asked and waits, or\n> proceeds, on a question nobody will ever see.\n>\n> **Until the local side lands, read the mailbox from the remote peer**\n> (`mailbox` with no `message`, or `survey`), or read `claims.json` on the\n> coordination branch directly.\n>\n> **The local-side change required** (in the Python `agentsync` package — a\n> separate repo, deliberately not modified here):\n>\n> 1. **Parse and preserve `notes`.** The claims-file reader must round-trip the\n>    top-level `notes` key. Today an unrecognised key risks being dropped on\n>    the next local write, which would *delete* remote peers' notes. Preserving\n>    unknown top-level keys is the minimum safe change and should land first,\n>    independently.\n> 2. **Surface notes in `survey()`** — add a `notes` field alongside\n>    `partners`/`stale_claims`, defaulting to `[]`.\n> 3. **Add a local `mailbox(message=None, to=None)` tool** mirroring this one:\n>    append `{from, to, message, at}` under the same compare-and-swap the local\n>    claim writes already use, and cap retention at the 200 most recent notes\n>    (this Worker evicts oldest-first at that bound — the two sides must agree,\n>    or they will fight over the file).\n>\n> The on-disk shape is already compatible in both directions: this Worker only\n> emits `notes` when non-empty, so a `claims.json` with no mailbox activity\n> stays byte-shape-identical to pure-local output.\n\n**On the phone again:**\n\n```\nYou: claim task \"tidy auth helpers\", touches src/auth/helpers.ts\nClaude: claimed.                    # laptop is \"done\" now, so it no longer blocks\n\nYou: (later) release note \"helpers tidied\"\nClaude: released.\n```\n\nEvery one of those steps was a compare-and-swap against the **same**\n`claims.json` on the **same** branch. The laptop never saw a remote peer; the\nphone never saw a git repo. Same mesh.\n\n## Configuration\n\nNon-secret config lives in [`wrangler.toml`](./wrangler.toml). Every var ships a\ndefault so the Deploy button works with no edits; change `REPO` to your own repo.\n\n| Var          | Default                          | Meaning                               |\n| ------------ | -------------------------------- | ------------------------------------- |\n| `REPO`       | `jarmstrong158/agentsync-remote` | `owner/name` of the coordination repo.|\n| `AGENT_ID`   | `jonny-mobile`                   | This peer's id in `claims.json`.      |\n| `BRANCH`     | `agentsync`                      | The coordination branch.              |\n| `CLAIMS_PATH`| `claims.json`                    | The coordination file.                |\n\nTwo **secrets** are set in the dashboard, never in the repo: `AUTH_TOKEN` (locks\nthe endpoint) and `GH_PAT` (GitHub Contents read/write on the coordination repo).\n\nPoint local agentsync and this Worker at the **same** `REPO` + `BRANCH` +\n`CLAIMS_PATH` and they share one mesh.\n\n## Reliability\n\nCoordination correctness under contention is measured, not asserted — the full\nwrite-up is [`docs/RELIABILITY.md`](./docs/RELIABILITY.md), reproducible with the\ncommands below.\n\n- **49 / 49 automated tests green**, covering five distinct CAS race scenarios\n  and all three overlap-detection modes (exact · directory-containment · glob,\n  each in both directions).\n- **1000 simulated concurrent races** (`test/stress-cas.test.ts`, seeded\n  `0x5eed`): **0 lost claims, 0 double-grants**, and every race hit a real 409\n  retry — so the number isn't inflated by trivially-serialized runs.\n- **Fail-closed auth is tested** — wrong token, unset `AUTH_TOKEN`, and non-`/mcp`\n  paths all 404; a missing `GH_PAT` surfaces a named error, not a silent failure.\n- **The MCP handshake is hardened** — `initialize` makes no network call, so it\n  answers instantly even on a cold isolate; it negotiates the client's requested\n  protocol version rather than rejecting a slightly-ahead client; and an\n  unexpected throw anywhere becomes a well-formed JSON-RPC error, never a bare\n  `500` a reconnecting client would read as a hard failure.\n- **Structured logs make failures visible** — every request emits one line of\n  JSON (`request` / `auth` / `handshake` / `tool_call` / `error`, with the path\n  token always redacted), so a dropped handshake or a slow tool call is one\n  filter away in Cloudflare Workers Logs (`[observability]` is on in\n  `wrangler.toml`).\n\n**Honest caveat:** the thousand races are simulated *in-process* — GitHub's 409\ncompare-and-swap is reproduced by a fetch-mocked fake ([`test/helpers.ts`](./test/helpers.ts)),\nnot exercised over the live API. The results validate the Worker's coordination\n*logic* — which is what runs in production — not GitHub's API, the network, or\nreal-world latency.\n\n## Maintainer / local development\n\nThe canonical repo (`jarmstrong158/agentsync-remote`) deploys via GitHub Actions\n→ Wrangler ([`.github/workflows/deploy.yml`](./.github/workflows/deploy.yml)):\nevery push to `main` runs the suite and only deploys if it's green. That workflow\nis guarded to the canonical repo, so a fork you created with the Deploy button\nnever tries to run it — your fork redeploys through Workers Builds instead.\n\nLocal checks (no Cloudflare credentials needed):\n\n```bash\nnpm install\nnpm test          # vitest, fetch-mocked — no live GitHub calls\nnpm run typecheck\nnpx wrangler deploy --dry-run --outdir dist   # bundle check\n```\n\n## See also\n\n- [agentsync](https://github.com/jarmstrong158/agentsync) — the local transport\n  (Claude Code on your machine). Same file, same rules; sibling transport.\n- [`DESIGN.md`](./DESIGN.md) — the CAS translation, overlap semantics, and why\n  the MCP handler is stateless and hand-rolled.\n- [`docs/RELIABILITY.md`](./docs/RELIABILITY.md) — the measured reliability\n  report: 49/49 tests, five CAS race scenarios, and 1000 seeded contention races.\n- [Deploy to Cloudflare buttons](https://developers.cloudflare.com/workers/platform/deploy-buttons/)\n  — Cloudflare's docs for the one-click deploy flow used above.\n\n## Related\n\n- [agentsync](https://github.com/jarmstrong158/agentsync) — the local git-native original this transport mirrors — and the [xylem](https://github.com/jarmstrong158/xylem) stack hub.\n",
  "bytes": 16802,
  "sha": "08a1d2773b07c2927ec4378225c834db8c30889eb7b8a5ef0fc50889eb5c6651",
  "repo_slug": "jarmstrong158/agentsync-remote",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_jarmstrong158_agentsync_remote_31eba849/readme"
}