sow.yml Reference
sow.yml is the single configuration file of a managed workspace. It sits at the
workspace root, declares which repositories and distributions exist, and holds the
membership policy and signing settings that every build applies. Plain mode
(sow create) never reads it.
This page lists every field the parser accepts. Anything not listed here is rejected — there are no undocumented keys and no keys reserved for future use.
How the file is read
SOW parses sow.yml with a strict decoder. Practically, that means:
- Unknown fields are errors, not warnings. A typo like
repositories:instead ofrepos:fails the command with exit code2and names the offending line. - Exactly one YAML document. A
---separator introducing a second document is an error. - Regular file only. A symlink at
sow.yml, or a file larger than 16 MiB, is rejected before parsing. - Defaults are filled in at parse time, not written back to disk. Run
sow config show --allto see the fully expanded form.
Some of the file is machine-maintained. sow init, sow repo new, sow repo rm,
sow dist new, and sow dist rm rewrite sow.yml atomically as part of their
transaction. Membership policy and signing are yours to edit by hand; there are no CLI
flags that set them.
After any hand edit, run sow config check. It parses the file, cross-checks it against
the SQLite state of every initialized repository, and resolves every signing key
reference — without writing anything.
Top level
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
schema |
string | yes | — | Must be exactly sow/v3. Any other value is a configuration error. |
architectures |
list of strings | no | [x86_64, aarch64] |
The CPU families this workspace is allowed to manage. |
repos |
map | no | empty | Repository name to repository configuration. |
targets |
map | no | empty | Publication target name to target configuration. |
The configuration value must be exactly schema: sow/v3.
architectures
This is a ceiling, not a target. It declares which architectures SOW may accept at all; individual distributions inherit the whole list unless they narrow it.
Only two canonical families are supported today: x86_64 and aarch64. The DEB
ecosystem names are accepted as input aliases and normalized at the parse boundary:
| You may write | Stored and displayed as |
|---|---|
x86_64, amd64 |
x86_64 |
aarch64, arm64 |
aarch64 |
So architectures: [amd64, arm64] and architectures: [x86_64, aarch64] are the same
configuration. Writing both aliases of one family — [amd64, x86_64] — is a duplicate and
fails:
noarch (RPM) and all (DEB) are not architectures here. They are neutral packages,
projected into every applicable view at build time, and the parser rejects them in this
list. An unsupported value such as riscv64 fails immediately:
The list may be present or absent, but it may not be empty.
Repository
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
protected |
bool | no | false |
When true, sow repo rm refuses to delete this repository, even with -f. |
signing |
map | no | none | Package and metadata signing settings, see Signing. |
dists |
map | no | empty | Distribution name to distribution configuration. |
protected
protected: true is a guard against deleting a whole repository by accident. It blocks
exactly one thing — repository removal:
That is exit code 6. Everything else keeps working normally: you can still add, rm,
build, create and delete distributions. To actually remove a protected repository, edit
sow.yml to set protected: false, confirm with sow config check, then run
sow repo rm.
Repository names
Repository and distribution names share one grammar: they must match
[a-z0-9][a-z0-9._-]* — lowercase letters, digits, dot, underscore, hyphen, starting
with a letter or digit. Uppercase is rejected, because the name becomes a directory and
must behave identically on case-sensitive Linux and case-insensitive macOS filesystems.
These names are reserved and rejected: ., .., .sow, pool, dists, sow.yml,
workspace.lock, workspace-ops, repo-locks. Two repository names that would collide
in the state directory — say db and db.db — are also rejected:
See Repository Layout for why.
Dist
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
format |
string | yes | — | rpm or deb. A distribution holds exactly one format. |
architectures |
list of strings | no | inherits workspace list | Narrows this distribution to a subset of the workspace families. |
limit |
integer | no | 0 |
Maximum versions to keep per package name and architecture; 0 keeps all. |
exclude |
list of rules | no | empty | Rules that keep matching packages out of this distribution. |
format
format is the only field sow dist new sets from the command line, and it cannot be
changed afterwards — an RPM distribution never becomes a DEB one. A package whose format
does not match is simply not a candidate for that distribution.
architectures
Omit this field and the distribution inherits the workspace list, which is what you want
almost always. Declare it only to narrow: an el9 distribution that should be x86-only
in an otherwise dual-architecture workspace.
The list must be a subset of the workspace list, and it may not be empty:
Adding a family here marks the distribution dirty; the next sow build renders the new
view. Removing a family that is still referenced by existing membership or by the built
generation is refused by config check and by every write command.
limit
limit caps how many versions of one package survive in this distribution. The grouping
key is (binary name, native architecture), so an x86_64 build and an aarch64 build of
the same package are counted separately, and a noarch/all package forms its own group.
0(the default) keeps every version.N > 0keeps the newestN, comparing RPM EVR or Debian version with the native ordering rules of each ecosystem.- A negative value is a configuration error.
With limit: 1, adding an older version alongside a newer one reports it as limited and
does not create membership:
Raising limit later does not resurrect versions that policy previously removed. The
package bytes may still sit in the pool, but membership is gone; re-add the file to bring
it back. See Membership Policy for the reasoning.
exclude
exclude is a list of rules. Each rule is a set of fields; within a rule the fields are
ANDed, within a field the patterns are ORed, and rules are ORed with each other. A package
is excluded if any single rule matches it.
That reads: drop every debug-flavored package, and separately drop aarch64 packages whose
name starts with test- or ends with -experimental.
Five fields are allowed:
| Field | Matched against |
|---|---|
name |
Binary package name |
source |
Normalized source name (RPM SOURCERPM, DEB Source) |
arch |
x86_64, aarch64, or neutral |
kind |
The classification below |
format |
rpm or deb |
kind is derived from the binary package name by its most specific suffix:
| Format | Name suffix | kind |
|---|---|---|
| RPM | -debuginfo |
debuginfo |
| RPM | -debugsource |
debugsource |
| RPM | -llvmjit |
llvmjit |
| DEB | -dbgsym |
dbgsym |
| DEB | -dbg |
dbg |
| any | none of the above | main |
Patterns are case-sensitive: either an exact string or a shell glob using *, ?, and
[...]. There is no regex, no version comparison, no negation, and no expression syntax.
An empty rule, an empty or untrimmed pattern, a repeated pattern within one field, and an
invalid glob are all configuration errors:
Policy order is fixed: exclude runs first, then limit. An excluded package is reported
per item and is not a failure:
Signing
Signing settings live on the repository, not on individual distributions, and cover two independent trust chains: the packages themselves, and the repository metadata clients verify before they trust anything else.
The tree is fixed. signing.rpm has packages and metadata; signing.deb has
metadata only — DEB packages are never re-signed, because APT verifies the archive
through Release, not through per-package signatures.
rpm.packages
| Field | Type | Default | Meaning |
|---|---|---|---|
mode |
string | never, or fill when key is set |
never, fill, or always. |
key |
key reference | none | The signing key. Required unless mode is never. |
trusted_keys |
list of key references | empty | Additional public keys accepted by fill. |
The three modes:
never— input bytes are stored verbatim. Whatever signature the package arrived with (including none) is what clients get.fill— sign packages that have no signature, or whose signature is not verifiable bykeyor one oftrusted_keys. Packages that already verify keep their exact bytes.always— every package must end up signed bykey. Packages already signed by it keep their bytes; everything else is re-signed.
fill is the default when a key is present, because it is the mode that preserves
upstream signatures. Setting mode to fill or always without a key is an error:
trusted_keys is a list of public keys whose signatures fill accepts as already-good.
The public half of key is always trusted and does not need to be listed. Repeating the
same reference twice is an error:
RPM package signing is the one operation that shells out: SOW calls the environment’s
rpm --addsign / rpm --resign against a private staged copy, never against your input
file. The private key must be available to the GPG environment that rpm uses.
rpm.metadata and deb.metadata
| Field | Type | Default | Meaning |
|---|---|---|---|
key |
key reference | none | Key used to sign repository metadata. |
passphrase |
passphrase reference | none | Passphrase for a protected private key. |
Configure rpm.metadata.key and every RPM architecture view additionally publishes a
detached repodata/repomd.xml.asc. Configure deb.metadata.key and every DEB
distribution additionally publishes a clearsigned InRelease and a detached
Release.gpg. Without a key, those files are simply not produced — repomd.xml and
Release are always written.
For file:// and env:// references SOW signs in-process; no gpg binary is involved.
Only agent:// requires gpg in the environment.
Changing a key reference or the fingerprint behind it marks the affected distributions
dirty, because the signing identity is part of each distribution’s built configuration
digest. The next sow build re-signs and produces a new generation.
Key references
A key reference is a string in one of these forms:
| Form | Example | Notes |
|---|---|---|
| Path | keys/repo-signing.asc |
ASCII-armored key file. A relative path resolves against the workspace root, not your current directory. |
file://<path> |
file:///secure/repo-signing.asc |
Same as above, written explicitly. Absolute paths therefore show three slashes. |
env://<VAR> |
env://SOW_METADATA_KEY |
The variable holds the armored key material itself, not a path. The name must match [A-Za-z_][A-Za-z0-9_]*. |
agent://<fingerprint> |
agent://7F721C4AD40F...CF3B |
Delegates to the ambient gpg-agent. The fingerprint is 16, 40, or 64 hex digits, case-insensitive. |
Any other scheme is rejected:
References are validated in two stages. Syntax is checked when the file is parsed and
fails with exit code 2. Whether the reference actually resolves is checked by
sow config check and by every write command, and fails with exit code 6:
Secret material never leaves the reference. sow config show --all prints the reference
and the resolved fingerprint, and nothing else:
Private keys and passphrases are never written to sow.yml, SQLite, the operation log,
JSON output, or error messages.
Passphrase references
passphrase accepts the same path, file://, and env:// forms as a key reference —
but not agent://, since a passphrase is a value, not a key handle.
Two rules apply:
-
A passphrase without a key is an error. It has nothing to unlock.
-
A passphrase alongside an
agent://key is an error. The agent owns the private key and handles its own prompting; a second passphrase channel would be ignored.
Publication targets
Each target binds one configured Repository to a storage namespace. Target names use the same lower-case name grammar as repositories.
| Field | Required | Meaning |
|---|---|---|
repository |
yes | Existing Repository owned by this target. |
provider |
yes | filesystem or r2. |
endpoint |
yes | Canonical file:///absolute/path without trailing slash, or canonical https://host for R2. |
region |
R2 | Must be auto; forbidden for filesystem. |
bucket |
R2 | Lower-case canonical bucket name; forbidden for filesystem. |
prefix |
yes | Relative public-tree prefix; empty means the storage namespace root. |
credential |
R2 | env://NAME or file:///absolute/path; inline secrets are forbidden. |
public_endpoint |
yes | Canonical URL ending in /, used for public content/absence verification. Filesystem accepts https://, http://, or file://; R2 requires HTTP(S). |
max_cache_ttl |
yes | Canonical bounded non-negative Go duration, including explicit 0s; overflow is rejected. |
authoritative_workspace |
yes | Must be true. |
single_writer |
yes | Must be true. |
exclusive_write_authority |
yes | Must be true. |
The three authority booleans are explicit safety acknowledgements, not defaults. Targets on the same storage may not have overlapping prefixes; filesystem targets may not resolve to overlapping effective paths. These rules protect conditional publication and GC from competing writers.
For provider: filesystem, configuration validation checks URL shape and overlap. At
publication time the endpoint directory itself must already exist, must not be a symlink,
and must resolve to one canonical real directory. SOW creates the configured prefix below
that endpoint, not the endpoint itself.
The first publish durably binds the Repository, provider storage identity, and prefix. Later edits
to the target name, public_endpoint, or max_cache_ttl require explicit operator confirmation with
sow publish TARGET --rebind. Provider,
storage endpoint, region, bucket, prefix, and Repository identity cannot be rebound; configure a new
target instead. Every accepted rebind appends an immutable binding revision. Pending maintenance
blocks TTL changes, and filesystem conditional-delete maintenance also blocks a public-endpoint
change.
R2 credentials are private references. The environment variable value or referenced file must contain one strict JSON document, not a path or shell assignment:
An optional temporary credential may add "session_token":"...". Unknown fields,
trailing data, missing access/secret values, and documents larger than 64 KiB are rejected.
config show, JSON output, and the public tree never contain the credential material.
Complete example
A workspace with two repositories: a protected production repository signing both metadata chains and filling in missing RPM signatures, and a scratch repository with no signing and no policy.
Validate it before you rely on it:
What is not in sow.yml
Some things you might expect to configure are deliberately not configurable:
- Repository paths. A repository always lives at
<workspace>/<name>. There is nopath:field. See Repository Layout. - APT components. Always
main. YUM has no component concept. - Architecture views. Derived from
architecturesand the package headers, never declared per package. - Inline secrets. Targets accept only credential references; key and passphrase material likewise stays behind a reference.
- Automatic retention counts. Retention is an explicit
sow retain add/rmoperation, not a rolling count in configuration.
See also
sow config— the commands that read this file- Membership Policy — how
excludeandlimitbehave over time - Signing Model — the two trust chains explained
publish,retain,gc, andexport— delivery lifecycle commands- Exit Codes — what
2and6mean here