Optimistic concurrency with If-Match: edit Sume Format files by API
Edit a Sume Format's files over the API without overwriting another writer: each write carries the file's blob sha, and If-Match guards the whole package.

To edit a Sume Format's files without overwriting someone else's change, read the package with GET /v1/formats/{handle}/{slug}/contents?recursive=1, then commit with PUT, sending the blob sha of each file you replace and the package sha in an If-Match header. If the file or the package moved since your read, Sume answers 409 and commits nothing.
The Contents API is shaped like GitHub's Contents API on purpose, so a coding agent can edit a Format the way it already edits a repository. The facts below come from Editing a Format package and the Sume API reference, read on 2026-09-26. For what a Format is, see What is a Sume Format?
What is optimistic concurrency, and how does Sume use it?
Optimistic concurrency means writers never take a lock. Each write names the version it was based on, and the server refuses it if that version is no longer current. The Contents API has two such checks, and they stack:
- The per-file
shais the git blob sha of the file as stored. Replacing or deleting an existing path requires it, so two agents editing the same file cannot silently overwrite each other. If-Matchcarries the package's own sha. Two agents editing different files each hold a per-fileshathat is still current, so both writes would land; the package sha closes that gap. It is additional: per-file checks still apply.
How do I read a Format's files?
Reads need an API key with formats:read. GET …/contents lists the package root, and GET …/contents/{path} returns one file, base64-encoded, with its sha. A path that names a directory returns that directory's entries.
Add ?recursive=1 (or recursive=true) to the root listing to get every file with its body in one call, sorted by path, with no dir rows. The sha on each row is the precondition a write takes, so one recursive read is enough to start editing.
How do I commit one file, or several in one commit?
Writes need formats:write. PUT …/contents/{path} takes a message, the whole new file as base64 content, and sha when the path already exists (omit it to create a file). It is a replace, not a patch. DELETE …/contents/{path} takes message and sha, and SKILL.md cannot be deleted. The commit's author is always the key's owner; an author or committer in the body is ignored.
PUT at the package root takes a files list and writes all of them as one commit, one version bump, and one new package_sha. files is a change set: paths it does not name are kept as they are. If any entry's sha is stale, or the resulting package breaks a rule, nothing is committed.
curl -sS -X PUT "https://api.sume.com/v1/formats/acme/product-promo/contents" \
-H "Authorization: Bearer $SUME_API_KEY" \
-H "If-Match: $PACKAGE_SHA" \
-H "Content-Type: application/json" \
-d '{
"message": "edit plan, add a note",
"files": [
{ "path": "references/plan.md", "content": "IyBQbGFuCg==", "sha": "3f7b…" },
{ "path": "references/new-note.md", "content": "IyBOZXcK" }
]
}'How is If-Match different from an ETag?
In standard HTTP, If-Match carries an entity tag (ETag) that the server returned on an earlier response. On the Contents API it is not an opaque ETag: it is the package sha, a bare 40-character hex string. Read it from package_sha on the Format record, or from commit.tree.sha in your last write's reply; the two are the same value.
- A stale value is refused with
409 format_package_sha_mismatch, anderror.details.package_shacarries the current one, so you can re-read and retry in one round trip. - The header works on the root
PUT, onPUT …/contents/{path}, and onDELETE …/contents/{path}. - Formats published before
package_shaexisted carry none, and they ignore the header instead of answering a409you could never satisfy.
Which errors should my code handle?
A write either commits or fails. There is no path where the Format changes but the commit does not exist.
| Status | `error.code` | What happened |
|---|---|---|
409 | format_content_sha_required | The path already exists and you sent no sha. Read it, then retry. |
409 | format_content_sha_mismatch | Your sha is stale: someone else committed first. Re-read and retry. |
409 | format_package_sha_mismatch | Your If-Match package sha is stale. error.details.package_sha holds the current one. |
400 | skill_path_invalid, skill_frontmatter_invalid, skill_limit_exceeded, … | The resulting package broke a rule. Nothing was committed. |
404 | format_content_not_found | The Format exists but holds nothing at that path. |
403 | insufficient_scope | The key lacks the scope, or it is a service-account key, which cannot create or edit packages. Mint a new key. |
503 | format_git_unavailable | Package history could not take the commit, so nothing was saved. Retry. |
What does the Contents API not do?
It is authoring, not execution. Editing a package never touches a run already in flight, because each run reads the package it started with; Sume Format versions covers how to tell which edit a run used.
- No public git endpoint and no clone URL. Reverting, blame, and history browsing are not part of this surface yet.
- No partial writes:
contentis always the whole file. - No deletes inside a batch: remove a file with
DELETE …/contents/{path}. - A batch may name at most 1000 paths, and a package holds at most 100 MiB per file and per package. The file rules are in How to write a SKILL.md recipe.
Sources
Related posts
Written by Sume