Igor

A build log about protocol archaeology, three wrong guesses, and refusing to trust my own mocks.

The token bill that started it

Claude Design (claude.ai/design) is a nice place to keep a design system, but the files live there, on the server. The obvious way to get them onto disk is to ask an agent: "read every file in this project and write it locally." That works. On one large project I measured, it also cost about 665,000 tokens — because every byte of every file has to pass through the model's context on the way out.

That number is silly. Moving bytes from A to B is not a reasoning task. The model is being used as a very expensive cp.

So I wanted a plain CLI that syncs a Claude Design project to a local directory the way git syncs a repo — clone, pull, push, status, diff — where the file bytes never touch a model's context. The same pull that cost 665k tokens should cost one summary line:

pulled 103, unchanged 0, binary 6 (660.1 KB)

Enter fullscreen mode Exit fullscreen mode

There was one problem: the endpoint that Claude Design speaks to is undocumented.

Protocol archaeology

There's no public spec. The endpoint is an MCP server, so the shape of the transport is known (JSON-RPC, tool calls), but which tools exist, what arguments they take, and what they return — that's all unwritten. The only way to learn it is to call it and write down what comes back.

That's fine; archaeology is a legitimate way to learn an API. The trap is what you do with what you learn. It is very easy to call an endpoint a few times, form a belief about how it behaves, encode that belief into a mock, and then test your code against the mock. Everything goes green. You ship. And you've tested nothing except that your code agrees with your own assumptions.

I know this because I got three protocol facts wrong before I measured them, and a green mock would have hidden every one:

  1. write_files returns a map, not a list. I assumed a list of results, one per file. It's keyed by path. A mock returning a list would have passed my tests and mis-parsed the real server.

  2. The "you don't have access to this project" case is an HTTP 403, not a tool-level error. I'd modelled it as an error object inside a normal 200 response. It's a status code — a different code path entirely.

  3. Binary detection is by content, not by file extension. I assumed the server keyed off the filename. It reads the bytes. A .txt full of NULs is binary to the server; an extensionless script is text.

None of these are exotic. They're the kind of thing you're 80% sure about and wrong. So the rule became: mocks test my own handling; the protocol facts get verified against the real server.

Concretely, the repo has a PROTOCOL.md where every documented claim has a corresponding live test (go test -tags=live) that hits the actual endpoint. If Anthropic changes the server and a claim stops being true, that test goes red — instead of the tool silently doing the wrong thing to your files. The mocks still exist, but they only ever test my own logic, never a fact about the world.

The part I was more scared of than tokens

Once bytes stop flowing through a model, the tool is just a sync engine talking to a server — and sync engines lose data. That became the second obsession after the token cost. A few of the decisions:

  • Every file is written temp-then-rename. A killed process never leaves a half-written file. This matters more than it sounds: a truncated file that also never made it into the tool's ledger looks, on the next run, like you edited it — so the tool would "helpfully" offer to overwrite your work. Atomic writes kill that whole class of bug.

  • Conflicts are decided on bytes, never on timestamps or etags. If both sides changed, that's a conflict, full stop. An etag comparison literally cannot see the both-sides-changed case.

  • --prune deletes only what it can prove was ours and unmodified. Untracked file → not ours, leave it. Locally edited → a conflict, don't touch it. The proof is an on-disk ledger, and twice during this project an attempt to make that proof "smarter" taught the prune loop to delete something it shouldn't have. Both were caught by tests, and both are written up in the repo, because a deleted-someone's-file bug is worth a paragraph so the next person doesn't reinvent it.

  • The credential is read, never written, never printed. The tool reads Claude Code's existing OAuth token so there's no separate login. It will not even wrap that token into an error string. (Refreshing it would rotate the refresh token out from under Claude Code, so it doesn't; an expired token is reported, and the fix is to run any claude command.)

stdlib only

The whole thing is a single Go binary with zero third-party dependencies — go.mod names none. The reason is auditability: everything that parses untrusted bytes from an undocumented server and touches your credential is the Go standard library plus this one program. staticcheck, govulncheck and the release tooling run in CI without ever entering the dependency graph. If you're going to point a tool at an unofficial endpoint with your OAuth token, "you can read all of it" is worth a lot.

The honest caveat

The endpoint is undocumented and unofficial. A server deploy can break it. I can't make that risk zero — but I tried to make it visible rather than hidden: the live tests are the tripwire, and the tool refuses and reports rather than guessing when the server answers in a shape it doesn't recognise.

It's called dsx, a single binary, macOS and Linux, MIT. If you use Claude Design and have felt that token bill — or you just like protocol-archaeology writeups — the code and the PROTOCOL.md are here:

https://github.com/somework/dsx

Happy to answer questions about the protocol or the data-loss reasoning.