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.md at the package root is necessary. Its frontmatter name must be equal to the slug of the Format. You cannot delete it.
  • Files are at the root, or one directory level under references/ or agents/. 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 .txt files 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

Statuserror.codeWhat happened
403insufficient_scopeThe 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.
409skill_slug_takenYour workspace already has a Format with that slug. A create call never overwrites a Format. Select a different slug.
409skill_slug_reservedA Format by Sume holds that slug for all workspaces. Select a different slug.
404format_not_foundThe 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.
404format_content_not_foundThe Format exists but holds nothing at that path.
409format_content_sha_requiredThe path already exists and you did not send a sha. Read the path. Then retry.
409format_content_sha_mismatchYour sha is stale, because a different writer committed first. Read the path again. Then retry.
409format_package_sha_mismatchThe package sha in your If-Match is stale. error.details.package_sha holds the current package sha.
400skill_path_invalid, skill_frontmatter_invalid, skill_limit_exceeded, …The new package broke a rule above. The API committed nothing.
503format_git_unavailableThe 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.