Directory layout
A flag namespace is a directory tree. This page is the reference for the tree's structure: where files go, what they're named, and the file-system requirements implementations rely on.
Tree structure
A flag namespace's manifest is rooted at a directory whose name doubles as the namespace slug. The full layout:
<namespace-slug>/
namespace.toml # optional
flags/ # zero or more flag files
<flag-key>.toml
...
segments/ # optional; only when at least one segment is defined
<segment-key>.toml
...
The smallest valid flag namespace is a directory containing one flags/<key>.toml file. Everything else — namespace.toml, segments/, the env list, per-env flag blocks — is scaffolding that earns its place when the flag namespace grows.
Concrete example
payments/
namespace.toml
flags/
checkout-redesign.toml
express-checkout.toml
payment-retry-v2.toml
segments/
beta-users.toml
checkout-redesign-rollout-10.toml
internal-employees.toml
Optional entries
namespace.toml— OPTIONAL. When absent, the flag namespace runs in untyped-env mode with every namespace-level field at its default and slug inferred from the directory name. See 04-namespace.md.flags/— OPTIONAL as a directory but expected in any non-empty flag namespace. Its absence emitsW011once any flag declaration would otherwise be expected.segments/— OPTIONAL. Only needed when at least one rule references a segment by name. Rules may also carry an inline predicate; see predicates.
Forbidden entries
| What's forbidden | Why | Diagnostic |
|---|---|---|
Files at the flag-namespace root other than namespace.toml and .exdignore | Keeps the root surface minimal | (silently ignored; not packaged) |
Subdirectories under flags/ or segments/ | One file per entity is the contract | W009 |
| Symbolic links anywhere in the tree | Symlinks compromise reproducibility — the same tree may resolve differently on different machines | E018 |
File naming
Flag files
Each flag lives in its own file at flags/<flag-key>.toml.
- The filename without the
.tomlextension is the flag key. There is noflag.keyfield in schema 0.1; the filename is the single source of truth. - The filename stem MUST match the key pattern:
[a-z][a-z0-9_-]*, max 63 characters. Violations raiseE031and the file is dropped before per-file lint. - The filename MUST end in the lowercase extension
.toml. Files with any other extension (e.g.,.TOML,.tom,.toml.bak) are silently ignored. - Flag-key uniqueness falls out of the filesystem: two regular files in the same directory cannot share a stem.
Segment files
Same rules as flag files: segments/<segment-key>.toml, lowercase .toml, key pattern, unique stems. Violations raise E032.
Namespace file
The namespace descriptor lives at namespace.toml. Its filename is fixed; renaming it has the same effect as deleting it (the linter falls back to defaults and infers the slug from the directory name).
Files the linter ignores
The linter operates only on regular files inside flags/ or segments/ whose names end in .toml. Everything else is silently ignored:
- Non-
.tomlextensions (README.md,.DS_Store,flag.toml.bak). - Subdirectories (even if their name ends in
.toml). - Files at the flag-namespace root other than
namespace.toml.
This lets teams keep auxiliary artifacts (READMEs, ownership docs, ignore files) alongside the manifest without affecting linting.
Identifier patterns
Two character classes are used throughout the manifest:
| Class | Pattern | Used for |
|---|---|---|
| slug | [a-z][a-z0-9-]* | tenant slug, namespace slug, environment slug |
| key | [a-z][a-z0-9_-]* | flag key, segment key, variant key |
Rules:
- Lowercase ASCII only. Unicode letters, uppercase letters, dots, slashes, and other punctuation are forbidden.
- First character MUST be a lowercase ASCII letter. First character MUST NOT be a digit, hyphen, or underscore.
- Maximum length is 63 characters for every slug and every key.
- Underscores are permitted in keys (flag, segment, variant) but MUST NOT appear in slugs (tenant, namespace, environment).
- All comparisons are byte-exact. The slug pattern is lowercase-only, so two slugs differing only in case can never both be valid.
TOML dialect
Every file in a manifest is a TOML 1.0.0 document.
| Aspect | Rule |
|---|---|
| Encoding | UTF-8. A leading BOM (U+FEFF) is accepted but ignored. |
| Line endings | LF (\n) or CRLF (\r\n). Mixed within one file is accepted but discouraged. |
| Comments | Permitted anywhere TOML allows them. Comments are NOT preserved when the server canonicalizes a manifest; they live only in the source files. |
| Duplicate keys | Forbidden by TOML. The parser rejects them; the linter records the parse error as E001. |
| Inline vs. table syntax | Both forms are accepted wherever TOML permits them. The choice carries no semantic meaning. |
File-system requirements
Case sensitivity
Flag keys, segment keys, and environment slugs are case-sensitive. Production and production are different identifiers. On case-insensitive file systems (macOS APFS, NTFS, FAT32) two files whose names differ only in case will collide before the linter ever sees them — that's a file-system error, not a manifest error. The slug/key patterns enforce lowercase, so the question never arises if you follow them.
Permissions
The reference linter requires only read permission. Write tools (exd manifest push, exd manifest pull) require the usual read/write permissions for the directory and its parent. No special UNIX permission bits or extended attributes are relied on.
Maximum file count
No hard maximum. The server's archive size limits (50 MB uncompressed, 5 MB compressed; see packaging) are the only ceilings.
Maximum file size
Each individual .toml file MUST NOT exceed 256 KB. Larger files are rejected with E019. Typical flag and segment files are a few hundred bytes to a few kilobytes; the cap exists to prevent runaway JSON variant payloads.
Traversal order
When the linter (or any tool) walks flags/ or segments/, entries are sorted by lexicographic byte order of the path. Deterministic ordering is what makes "previous file" pointers in diagnostics reproducible across runs.
Tools that emit diff or report output across versions also use lexicographic order so two runs against the same flag namespace produce identical output.
Why one file per entity?
Three reasons (informative):
- Agent context efficiency. An agent asked to modify
flags/checkout-redesign.tomlcan fetch exactly that file and receive only the relevant content. A monolithic config file containing hundreds of flags wastes context on unrelated entries and increases the risk of hallucinated edits to adjacent flags. git blameand ownership. Each file has its own complete commit history.git blame flags/checkout-redesign.tomlshows who changed each field, without noise from unrelated flag edits. PR reviews are scoped: a reviewer of a checkout change is not presented with a diff touching an unrelated payments-retry flag.- Minimal diffs. Changing a single rule on one flag produces a diff touching exactly one file. PR review is fast, merge conflicts are rare, and CI gets a deterministic file path as a cache key.
Empty flag namespace
A flag namespace whose flags/ and segments/ directories are both empty (or absent) is a valid manifest. If namespace.toml is also absent, the linter passes with no diagnostics: slug = directory name, every namespace field at its default, untyped-env mode in effect. If namespace.toml is present, it MUST be well-formed.
See also
- 04-namespace.md — what goes in
namespace.toml. - 05-flag.md — what goes in
flags/<key>.toml. - 06-segment.md — what goes in
segments/<key>.toml. - packaging — how the directory tree becomes a tar.gz for upload.
- diagnostics —
E018,E019,E031,E032,W009,W011.