Editing a Format package
Each custom Format is a small package of files. The package has a SKILL.md entry file
and optional notes under references/. The package also has a real commit history. With the
Contents API and an API key, you can read those files and commit changes to them. Thus, a coding
agent can edit a Format in the same way that it edits a repository.
The shape of this API is the shape of the GitHub Contents API. We selected this shape on purpose.
If you know gh api repos/{owner}/{repo}/contents/{path}, you know this API:
{handle}/{slug} is the same address that you use to invoke the Format. A read needs
formats:read. A write needs formats:write. The author of the commit is always the owner of the
key. The API ignores an author or committer in the body.
This API changes the Format package. It does not run the Format. A change to a package has no effect on a run that already started. Each run reads the package that it started with.
Create a Format
POST /v1/formats opens a Format and its package repository. This is the same as how POST /repos
opens a repository on GitHub. The call needs formats:write. The API creates the Format in your
key's workspace. There is no {handle} in the address. Thus, you cannot create a Format in a
workspace that is not yours.
auto_init (default true) commits a minimal valid SKILL.md. Thus, you can read and write the
Format through the endpoints below immediately. The intended next call replaces that file.
false is a 400: each Format package must contain SKILL.md, thus you cannot create an empty
Format. The body does not accept package files, because the Contents API is for package files. All
the files of a Format must obey one set of rules, for all writers.
Keep package_sha and contents_url from the reply. The first value is the If-Match
precondition for your next write. The second value is the address for that write. If your workspace
already uses the slug, the call answers 409. The call never overwrites a Format.
The API opens the repository before the catalog row. If the API cannot reach the package
history, the call fails with 503 format_git_unavailable. In this condition, the API creates no
Format. This prevents a Format with commits that no one can resolve.
Read the package
A request for one path returns the file, base64-encoded:
Keep the sha. It is the git blob sha of the stored file. Each write uses it as the
precondition. If the path is a directory (…/contents/references), the API returns the entries of
that directory, not a file.
Read the whole package at once
If you list the root and then get each path, you send one request for each file. Sometimes you want the full package, for example to load it into the context of an agent. In that condition, ask for the package in one call:
Each row is a file with its body, sorted by path. There are no dir rows,
because the paths show the directories. recursive=true also works. All other values, and no value,
give the plain root list above. The sha on each row is the same precondition that a write uses.
Thus, one recursive read is sufficient before you start to edit.
This parameter applies only to the list. …/contents/{path} gives the same answer with or without
the parameter.
Commit a change
PUT writes one full file and makes one commit. It replaces the file. It does not patch the file.
Send the full new body, base64-encoded.
commit.tree.sha is the new package identity of the Format. It is the same value that the record
of the Format shows. version is the display counter that the dashboard shows.
If the path already exists, send sha. To create a new file, do not send it. Two agents that edit
the same file cannot overwrite the work of each other without an error. After the first write, the
sha of the second agent does not agree with the file. For agents that edit different files of
one Format, refer to If-Match.
DELETE takes message and sha. It answers with content: null:
Commit several files at once
If you edit a folder one path at a time, each file costs one commit and one round trip. PUT at
the package root (with no {path}) takes a files list. It writes all the files as one commit,
with one version increase and one new package_sha:
Each entry obeys the single-file rules. content is the full file, base64-encoded. sha is
necessary when the path already exists. To create a path, do not send it. content in the reply is
the list of files that this commit wrote.
files is a change set, not the package. If the Format holds a path and this body does not
name it, the API keeps that path as it is. Thus, an edit to two of twelve files never puts the other
ten at risk. As a result, you cannot delete a file with this call. To remove a file, use
DELETE …/contents/{path}. Thus, the API removes a file only when you ask for it, never because you
forgot to name the file.
If the sha of an entry is stale, or if the new package breaks a rule, the API commits
nothing. The API commits the full batch or no part of it.
If-Match
A per-file sha cannot say "the package has not moved since I read it". Two agents can edit
different files. Each agent holds a sha that is still current, thus the two writes land. But the
second agent planned its edit against an old tree. That tree was not there at the time of the write.
To close that gap, send the sha of the package. It is commit.tree.sha from your last write. It
is also the same value that the Format record shows as package_sha:
If the package changed after your read, the API refuses the write with
409 format_package_sha_mismatch. error.details.package_sha holds the current sha. Thus, you can
read again and retry in a single round trip. The header also works on PUT …/{path} and
DELETE …/{path}. It is an additional check, and the per-file sha checks still apply.
Here, If-Match is not an opaque ETag. It is the package sha, as a bare 40-character hex string.
Formats published before package_sha existed do not have a package sha. Those Formats ignore the
header. They do not answer with a 409 that you can never satisfy.
What a package may contain
These are the same rules that the dashboard editor enforces:
SKILL.mdat the package root is necessary. Its frontmatternamemust be equal to the slug of the Format. You cannot delete it.- Files are at the root, or one directory level under
references/oragents/. A path must not contain... A path must not be absolute. - File names must match
^[A-Za-z0-9][A-Za-z0-9._-]*$. A file name must not start with_or.. - Only
.md,.json,.yaml,.yml, and.txtfiles are permitted. - The maximum is 100 MiB for each file and 100 MiB for each package. A Contents batch can name a maximum of 1000 paths.
If a package breaks one of these rules, the API rejects it before it commits anything. For a
rejected path, the API answers with skill_path_invalid and gives you the full allowlist. Thus, the
first rejection gives sufficient data to correct the name.
Errors worth handling
| Status | error.code | What happened |
|---|---|---|
403 | insufficient_scope | The key does not have formats:read / formats:write, or it is a service-account key. A service-account key cannot create or edit packages. next_action is authenticate. You cannot patch scopes. Mint a new key. Missing scopes never give format_not_found. |
409 | skill_slug_taken | Your workspace already has a Format with that slug. A create call never overwrites a Format. Select a different slug. |
409 | skill_slug_reserved | A Format by Sume holds that slug for all workspaces. Select a different slug. |
404 | format_not_found | The Format is unknown, or it is not in the workspace of the key. For a team Format, you need a key created in that workspace. |
404 | format_content_not_found | The Format exists but holds nothing at that path. |
409 | format_content_sha_required | The path already exists and you did not send a sha. Read the path. Then retry. |
409 | format_content_sha_mismatch | Your sha is stale, because a different writer committed first. Read the path again. Then retry. |
409 | format_package_sha_mismatch | The package sha in your If-Match is stale. error.details.package_sha holds the current package sha. |
400 | skill_path_invalid, skill_frontmatter_invalid, skill_limit_exceeded, … | The new package broke a rule above. The API committed nothing. |
503 | format_git_unavailable | The package history did not accept the commit, thus the API saved nothing. Retry. |
We designed the last error on purpose. A write either commits or fails. The Format cannot change without a commit.
What this is not
There is no public git endpoint and no clone URL. You cannot reach the history writer directly. Only this API makes commits. At this time, this API does not let you revert commits, read blame, or browse the history.