The fleet file — clanker.yml
The host-level file: one per host, owned by the host owner. It names the host and the repositories every engineer’s pod on that host holds, each at a pinned ref, and who may hold them. A repository’s own container settings are not here — each repository carries its standard dev-container file. Where it all runs is the host spec the fleet points at. From nothing to a first pod: Quickstart.
# yaml-language-server: $schema=https://clanker.net/schema/clanker-fleet.v1.json
version: 1
host: ./hosts/dev-host.yml # or a repo: reference, or the host spec inline
pod: # present → one POD per engineer; absent → one container per repo
default: acme/api # what a fresh pod shows first
repos:
- url: https://github.com/acme/shop
ref: develop # a branch (resolved at every read) or a 40-character sha
devcontainer: .devcontainer/devcontainer.json # the default
access: { github: { org: acme } } # REQUIRED — who may hold a pod with it
- url: acme/api # the bare slug is the same repository
ref: main
access: { github: { org: acme } }
- url: acme/handbook
ref: main
devcontainer: false # no container: a clone every member of the pod can see
access: { github: { org: acme } }
sync:
schedule: "0 6 * * *" # when the daily fleet-sync runs, optional
- Schema:
https://clanker.net/schema/clanker-fleet.v1.json. - Validate:
replicator validate clanker.yml. The report names the host, every enabled repository with the sha its ref resolved to, the file that was read and its ssh port, and everyenabled: falseentry as skipped.--offlinemakes no network call (entries are listed unresolved with a warning);--jsongives the same as data.
The fleet file lists the repositories a host runs. It does not describe a repository’s own staging, production or sites: that is the repository’s environments file, run by the same engine and unrelated to which hosts list the repository.
What a pod is
With pod: present, every engineer who provisions on this host holds ONE
pod named after them: a member for each repository that has a
dev-container file, a workspace clone for each one that does not — all
behind ONE public name and ONE address on your private network.
flowchart TB
subgraph pod["Pod dev-alice — one address on your private network"]
direction TB
door["Port 22 → the <b>selected</b> member"]
m1["Member <b>shop</b><br/>its database, its ssh port 2201<br/>runs /workspace/shop"]
m2["Member <b>api</b><br/>its ssh port 2202<br/>runs /workspace/api"]
ws["<b>/workspace</b> — the shared root, seen by every member<br/>shop · api · handbook (a workspace clone)<br/>CLAUDE.md · AGENTS.md · the <code>clanker</code> agent"]
door -. currently .-> m2
m1 --- ws
m2 --- ws
end
public["<b>dev-alice.your.zone</b><br/>and app-dev-alice.your.zone …"] -- "the selected member" --> door
Members share one network: they reach each other on localhost, and every
port a member declares is a port of the whole pod (which is why two members
cannot declare the same number). Each member keeps its own storage — its
clone, its database, its keys — and every checkout is a sibling in every
member: a member’s clone is also mounted, read-write, at
/workspace/<name> in every other member, beside the workspace clones. So
from any member ls /workspace lists every repository of the fleet; the
files are the ones each member runs. What stays a member’s own is its
runtime — its processes, ports, hooks and tmux session. (pod.siblings: false keeps each clone inside its own member; repos[].pod.sibling: false
keeps one clone out.) Nothing is lost on a teardown; only purge removes
storage.
The shared root is also a project for the coding agents: it carries a
CLAUDE.md you may edit, an AGENTS.md generated at every boot (every
repository, its path, its ssh port, which one the public name shows), a
clanker agent that works inside one repository, and the coordination
server. A session started at /workspace fans out one clanker per
repository and closes with the checks each repository names; a session
inside one repository follows that repository’s own instructions. The
members’ secrets are denied to that root session on purpose.
Without pod:, the fleet keeps the single-container shape: one container per
engineer per repository, each with its own name and address.
Fields
Strict throughout: an unknown key is an error, not a silent no-op.
version (required)
2.
host (required)
Where the fleet runs — a host reference as a path, a repository reference, or the host spec inline:
| Form | Resolved from |
|---|---|
./hosts/<name>.yml | a file relative to the fleet file |
repo:<owner>/<repo>@<ref>:<path>.yml | the host owner’s repository at that ref (a private one needs the workflow’s checkout_app_from) |
| an object | the host spec itself, in place |
--host on the command line overrides it: a host id, a path, or a reference.
repos[] (required, unless discover lists them)
| Key | Type | Default | Meaning |
|---|---|---|---|
url | repository URL or owner/name | — | The repository; the URL and the bare slug are one identity, and a repository listed twice is refused |
ref | branch/tag or 40-hex sha | — | Where the dev-container file is read. A branch means “whatever it is at read time” — the run summary records the sha it resolved to; pin a sha for reproducibility |
devcontainer | repo-relative path, or false | .devcontainer/devcontainer.json | The file to read at ref. false makes the entry a workspace: no container runs — the repository is cloned at /workspace/<name>, where every member sees it (needs pod:). A branch ref is fast-forwarded on every start and sync; a sha holds it at one commit |
access | object | — (required) | Who may hold a pod with this repository on this host, and how they reach it. Below |
enabled | boolean | true | false keeps the entry in the file but out of every read: not fetched, not converged. In a pod fleet its member leaves every pod like a removed one (storage kept), and it still counts for the ssh port table, so its port waits for it |
owner | team name (lowercase letters, digits, dashes) | — | The team that owns the repository. replicator plan shows it next to the entry, and a host’s gateway.admit.owners (hosts) admits by it. It grants nothing on its own |
pod.sshPort | 1–65535, not 22 | the port the pod already gave it, else the next free one | The member’s ssh port on the pod’s address (needs pod:). A port is given to a repository the first time the pod sees it and kept: inserting or reordering repos moves nobody, and a port a repository gave up is never handed to another. Pin it to choose the port yourself; two entries on one port are refused. A pod with no history starts from 2200 + the entry’s place among container entries, and keeps those ports from then on |
pod.start | auto | on-demand | clone | auto | How the repository joins a pod (needs pod:). auto: its container is always part of the pod. on-demand: a clone at /workspace/<name> until someone selects it, which starts that one container and touches no other. clone: only a clone — its dev-container file is never read. Below, “Many repositories” |
pod.sibling | boolean | true | With pod.siblings on, this clone is mounted in every member at /workspace/<name>. false keeps this one clone inside its own member (which still sees the others) |
No entry is flagged as the coordination repository. Every enabled entry is
tracked by the host’s coordination server, and push access to any of them is
what lets a person sign in. In a pod fleet the pod coordinates in one
workspace, named for the pod.default entry (else the first enabled one):
every member works in it, whichever member boots. A host spec with a
gateway: block needs at least one enabled entry (host up and
host converge refuse otherwise); a host without that block needs none.
The file is read at the sha the ref resolved to, so a push between two reads cannot straddle a run. A repository with no dev-container file at its ref fails naming the path, the ref and the sha:
fleet repo `acme/api`: no `.devcontainer/devcontainer.json` at ref `develop` (b2c…) — every fleet repo carries its dev-container file at the pinned ref
access
Who may hold a container of this repository on this host, and how they reach it:
| Key | Type | Default | Meaning |
|---|---|---|---|
github.org | login | — | Only members of this organisation may provision. Needs an org-read token by name in the host’s secrets store |
github.users | login[] | — | An explicit allow-list; needs no token |
ssh.keySource | github-keys | sshid | list | github-keys | Where authorised keys come from. github-keys = the login’s published keys |
ssh.users.<login>.sshid | string | the login | sshid.io username override |
ssh.users.<login>.keys | string[] | — | Static keys, for list |
tailscale.hostname | template (single label) | first label of the default hostname | The container’s name on your private network — single-container fleets only. In a pod fleet the pod has ONE address (pod.tailscale.hostname) and this key is a warning asking you to remove it |
Required, with github.org or github.users inside it. “Who may hold a
pod on this host” is a grant of the host owner’s compute, so the host owner
states it, in the host owner’s file — a repository cannot grant itself a
host, which is why access is refused inside a dev-container file and why
an entry without it is an error rather than an open door.
sync
| Key | Type | Default | Meaning |
|---|---|---|---|
schedule | five-field cron expression | — | When the daily fleet-sync re-reads every repository at its ref and converges every pod on the host |
policy — the organisation’s rules for environments
Optional. Rules the host owner sets once for every repository’s environments
(replicator infra diff|deploy --policy clanker.yml, see
Environments). They sit beside the built-in checks that never
delete a data resource, a DNS record or anything on a protected stage.
| Key | Type | Meaning | A refusal names |
|---|---|---|---|
allowedRegions | list of locations | The only locations a server, database or bucket may be created in, as the component spells them (hel1, WEUR, aws-eu-central-1); compared without regard to case | region |
requiredLabels | map of label to value | Labels every labelled resource (servers) must carry. A value of "" only requires the label | label |
protectedKinds | list of kinds | Kinds of resource never deleted or replaced on any stage, whatever the caller allows: bucket, database, dns, site, secrets, server | protected-kind |
maxStages | integer, 1 to 50 | The most stages one repository’s environments file may declare | stage-count |
pod — one pod per engineer
| Key | Type | Default | Meaning |
|---|---|---|---|
hostnames.default | template ending in .{zone} | dev-{user}.{zone} | The pod’s one public name, answered by the selected member. Must contain {user}; {repo} is refused (one name for the whole pod). Its first label names the pod |
hostnames.port | template ending in .{zone} | {port}-dev-{user}.{zone} | Every other public port of the selected member, by the port’s name. Must contain {user} and {port}; may contain {repo} (the member’s name) only with hostnames.portScope: member |
hostnames.portScope | shared | member | shared | Who owns a public port name. shared: one member per name, and the same name in two members is refused. member: names are per member — put {repo} in hostnames.port and two repositories can both publish app |
maxMembers | integer ≥ 1 | — | The most containers one engineer’s pod may run at once (auto entries plus the on-demand ones they started). Over it, validate and provision refuse and list the entries |
enforceBudget | boolean | false | true turns the check of the members’ summed hostRequirements against the host from a warning into a refusal |
tailscale.hostname | template (single label) | dev-{user} | The pod’s ONE name on your private network. Must contain {user}. Every member’s ssh is a port on it |
default | owner/name | the first container entry | What a fresh pod shows before anyone selects. Must be a container entry (a workspace has nothing to show) |
siblings | boolean | true | Every member’s clone mounted read-write in every member at /workspace/<name> — one engineer, one user, one trust domain (every member must declare the same remoteUser). false keeps each clone inside its own member |
All keys are optional — pod: {} takes every default. The pod’s templates
name every member; a member’s own file may repeat them or leave them out,
but a file declaring different templates is refused, because it would be
routed under names it never gets.
Adding a repository
A repository joins the fleet by describing itself; nothing about it is written into CLANKER.NET. Three steps:
-
List it here. Add an entry to
repos:with its URL, the ref to track and who may open it:repos: - url: https://github.com/acme/billing ref: main access: github: { org: acme } -
Describe it in the repository. A standard dev-container file at
.devcontainer/devcontainer.json(its settings undercustomizations.replicator), and, for anything it deploys,replicator.config.tsfor its own environments. A repository with no dev-container file is a workspace clone, not a container. -
Give it a thin workflow. One short file that calls the shared control plane by pin and passes the one secret. Everything else stays in the shared workflow, so a fix there reaches every repository:
name: Environments on: { workflow_dispatch: {} } permissions: contents: read jobs: staging: uses: clankerlabs/CLANKERNET/.github/workflows/replicator-deploy.yml@<40-character SHA> # v1 permissions: contents: read packages: read with: action: infra-diff stage: staging replicator_ref: <40-character SHA> # v1 secrets: REPLICATOR_STATE_TOKEN: ${{ secrets.REPLICATOR_STATE_TOKEN }}
The next fleet-sync adds it to every pod. The entry field of the pod
workflow takes any enabled container entry in this file as owner/name, and
names the valid ones when it is given another.
Reaching your repos
A pod has ONE address on your private network and ONE public name. The
onboarding card (the job summary of your provision) prints all of this
for your own pod; the shape, for an engineer alice on the host above:
| Port | Reaches |
|---|---|
22 | the selected member, whichever it is |
2201 | shop |
2202 | api |
ssh developer@dev-alice # :22 → the selected member → its tmux session
ssh -p 2201 developer@dev-alice # shop, whatever is selected
ssh -p 2202 developer@dev-alice # api
mosh developer@dev-alice # the selected member
scp -P 2202 file dev-alice:~/api/
Every member runs its own ssh service on its port, authenticates you with
its own entry’s access.ssh.keySource, and drops you into its workspaces
tmux session at the shared root — where a session works across every
repository. Every repository of the pod is a tmux session of its own,
named by the repository and opened in /workspace/<name> (so cd .. is
the root): this member’s own session runs the Procfile’s processes, each
other one holds a login shell in its directory. prefix s lists them,
Alt+Left/Right steps between them, prefix Tab returns to the last one,
and the right of the status bar reads <you>@<member>;
services.tmuxSession names the session of a standalone container only.
The pod shares one host key,
so a switch never changes what your client sees. The card also prints a
~/.ssh/config block so each member has a name of its own:
Host dev-alice
HostName dev-alice
User developer
Host dev-alice-shop
HostName dev-alice
User developer
Port 2201
Switching
select moves the public name (the apex and the port names) and the
port-22 front door to one member. Nothing restarts; established ssh
sessions keep their connection; only the next ssh dev-alice lands on the
new member.
sequenceDiagram
actor A as alice
participant API as member api
participant SHOP as member shop
participant P as public name
Note over P: shows api
A->>API: replicator select shop
API-->>P: route + port 22 now point at shop
Note over P: shows shop — a few seconds, nothing restarted
A->>SHOP: ssh dev-alice (port 22)
Note over API: still running, no public route<br/>ssh -p 2202 reaches it
Three ways, one result:
- From the app: the Pod screen’s DEV URL section (one tap per dev
container), or
/select <name>in a DM with your pod’s agent. The agent runs the pod’s ownfactory-select, so nothing new holds a credential, and only the pod’s owner can switch it. - From inside any member:
replicator select <name>(replicator selectalone lists the members and the current selection). No credential leaves the host; a member can only pick from its own pod. - From the workflow:
action: select, picking the dev container from theentrylist (the fleet’s container entries;currentkeeps what is shown, which is what a routineprovisionwants). The run then checks that the public name answers 200 on the member’shealthpath and that port 22 reaches it.
The member that is not selected keeps running, and its public PORT names keep answering (a public port label belongs to one member of the pod, so nothing contends for it); only the apex — and the path-routed ports on it — moves with the selection. DNS never changes on a switch: every member’s public names are published once, at provision.
Who may open a pod’s names
Every public name of a pod — the apex and each port name — is behind the
same GitHub sign-in as the app. The host’s ingress asks the coordination
server before every request; a browser without a sign-in for that name
is sent to the app (https://app.<zone>/auth/pod), which hands it a
single-use ticket back to the pod’s name, where it becomes a cookie for that
name alone, good for twelve hours and ended early when the person loses push
access to the fleet’s repositories. Nothing of the app’s own session reaches
a pod: a pod serves whatever its developer’s checkout serves, and a cookie
shared across the zone would be a token every pod could read
(client/AUTH.md, “Opening a pod”).
A member opts one port out of the gate with auth: none in its own file
(docs/devcontainer.md, “Who may open a public port”) — for a webhook or
callback target no browser sign-in can precede. The port a member’s
services.s3.public names is open by default, a presigned URL being its own
authorisation. The gate is the host’s: the fleet declares nothing, and the
coordination server learns the zone (the host’s first DNS zone) and the
app’s origin (https://app.<zone>) from the host spec at converge.
Keeping a host converged: fleet-sync
fleet-sync (the daily job from sync.schedule, or on demand) converges
every pod on the host to the fleet file. For each pod, each repository
in the fleet is compared with what the pod holds:
flowchart LR
r["A repository in the fleet…"]
r --> u["unchanged, or only its sha moved"] --> ku["member keeps running<br/>same container, same session"]
r --> c["its dev-container file changed,<br/>or a new image was published"] --> rc["member keeps running, reported <b>stale</b><br/>the engineer's own provision applies it"]
r --> a["added"] --> j["joins every existing pod<br/>nobody re-dispatches provision"]
r --> d["removed or disabled"] --> l["member leaves every pod<br/>storage kept, its public names pruned"]
r --> n["no longer allowed for this person"] --> o["left out of their pod<br/>never torn down"]
Three rules protect people. fleet-sync never restarts a running
container — nobody consented to that: a member whose dev-container file
changed, or whose image the run pulled past (a new release), keeps running
and its pod is reported stale, naming the container and why; the
engineer applies it with their own provision, when it suits them. A
moved commit alone is never a change — the clone lives on the member’s
storage and sync fast-forwards it. A pod whose owner cannot be
checked (the lookup failed rather than answered “no”) is reported as
failed and nothing runs on it. And someone allowed for nothing is
skipped and reported, never torn down — offboarding is an admin’s
explicit teardown with user:
(Quickstart). The run summary lists
every pod with its members, their shas and ports, the selection and the
outcome.
An engineer’s own sync is the same idea for one pod: every member
fast-forwards its clone and re-runs its updateContentCommand, every
workspace pulls — fast-forward only, never a reset, so a commit you made in
a workspace clone is never lost.
Discovery: a rule instead of a list
A fleet of twenty repositories does not have to name each one. A discover
rule lists an organisation’s repositories and expands to entries when the
file is read:
discover:
- org: acme
topic: replicator # only repositories carrying this topic
exclude: [acme-handbook, "legacy-*"] # names or globs
autoJoin: join # `plan-only` (the default) shows, never starts
access:
github: { org: acme }
repos:
- url: acme/billing # an explicit entry always wins: its own ref, owner, pod settings, `enabled: false`
ref: release
access:
github: { org: acme }
| Key | Default | Meaning |
|---|---|---|
org | — (required) | The organisation (or user) whose repositories are listed. Archived repositories are never taken |
topic | every repository of org | Only repositories carrying this topic. Without it, exclude is how you narrow the rule |
exclude | none | Repository names or globs (*, ?), matched against the name and against owner/name |
ref | the repository’s default branch | The ref every discovered entry is read at |
access | — (required) | The same block an entry has: who may hold a pod with these repositories |
devcontainer, enabled, owner, pod | as on an entry | Applied to every discovered entry (pod.sshPort cannot be pinned for a whole rule) |
autoJoin | plan-only | plan-only: the run lists what the rule found and adds no entry — nothing is read or started, so a topic drifting cannot start a container. join: they are entries like any other |
An expanded entry equals the entry you would have written by hand: same ref rules, same file read at the sha, same ssh port. Entries keep a stable order — explicit ones in file order, then each rule’s repositories by name — and ports follow the pod’s record (see “Many repositories”), so a repository that gains the topic takes the next free port and moves nobody. If two rules list one repository, the first keeps it; an explicit entry beats every rule.
replicator validate and replicator plan print each rule’s expansion
(discover acme topic replicator: 5 found, 2 joined, 3 pending (autoJoin: plan-only)),
list the entries a rule added (discovered), and carry "source": "explicit"|"discovered" on every row of --json. --offline cannot list an
organisation: the rule is reported as not expanded.
A listing that fails removes nothing. A rule whose listing cannot be read
(not found, a server error, a redirect, an empty answer, something that is
not a repository list) is reported unreachable and treated as unknown, not
as “no repositories”. fleet-sync keeps every member the pods already hold
from that organisation, pinned at the commit they were last converged at, and
says so; a developer’s own provision, sync and select, and a host
converge (which admits people by the fleet’s repositories), stop with the
same message instead of shrinking a pod or the sign-in. When the listing
answers again, a repository that lost the topic leaves every pod like any
removed member (its storage is kept).
Fleet status
replicator fleet status [--host <id>] [--json] shows every entry once, and
changes nothing:
fleet clanker.yml (host dev-host): 5 entries, 3 pod(s)
owner platform
acme/web@main → a1a1a1a · container · explicit · ssh :2201 · behind · 1/2 pod(s) current · last sync 2026-11-11T05:20:00.000Z
acme/api@b2b2… · container · explicit · ssh :2202 · in sync · 3/3 pod(s) current
no owner
acme/notes@main → c3c3c3c · workspace · explicit · not provisioned
acme/found@main · container · discovered · pending
discover acme topic replicator: 2 found, 1 pending (autoJoin: plan-only)
Each row has its owner (rows are grouped by it), kind, the commit its ref
resolves to, whether it was written or discovered, and its ssh port. With the
host’s pod records it adds how many pods hold the entry, how many of those are
at the current commit (in sync, behind, not provisioned), and when the
host last wrote a pod’s record, which is the last provision, fleet-sync or
select. A repository a pod still holds that the fleet no longer lists is
departed; one held while its organisation’s listing cannot be read is also
marked UNREACHABLE listing and is not reported as removed. An entry whose
dev-container file does not resolve is unreadable with the reason, a
plan-only find is pending, a disabled entry disabled.
--offline reads the fleet file alone (no repository, no host); --no-records
reads the repositories but does not ask the host. Neither writes anything.
Many repositories
A pod scales to twenty repositories without any of them naming another.
Ports stay put. A repository’s ssh port is recorded in the pod the first time the pod sees it. Adding a repository anywhere in the file, reordering the file, or disabling an entry changes no other member’s port; the new one takes the lowest port nobody has held. Removing a repository retires its port for good — adding it back later is a new entry with a new port — so a saved ssh command never reaches someone else’s repository. A pod created before this rule keeps the ports its members already run on.
Port names per repository. By default a public port name belongs to one
member (hostnames.portScope: shared), because the name alone makes the
public hostname. With twenty independent repositories, “app” and “api” will
repeat. Set hostnames.portScope: member and write the hostname template
with {repo}:
pod:
hostnames:
port: "{port}-{repo}-dev-{user}.{zone}"
portScope: member
Two repositories that publish app now answer at app-one-dev-alice… and
app-two-dev-alice…. The single public name still belongs to the selected
repository, a hostname that two members would still render alike is
refused, and the names are published once for the whole pod. Existing
fleets keep shared and their current URLs.
Only what you work on runs. Most repositories in a large fleet are read, searched and edited, not run. Mark them:
repos:
- url: acme/web # always running (the default, `start: auto`)
- url: acme/billing
pod: { start: on-demand } # a clone until you select it
- url: acme/handbook
pod: { start: clone } # a clone, never a container
pod:
maxMembers: 4
enforceBudget: true
An on-demand repository is validated like any member (its ports and names
are checked and reserved), but no container exists until an engineer selects
it: select starts that one container, moves the public name to it, and
recreates nothing else. It stays running until the pod is rebuilt without it
(provision, which keeps started members). Until then it is a clone in the
shared root, and its checkout is mounted in every member as usual. A clone
entry never starts and its dev-container file is never read.
maxMembers counts what runs; with enforceBudget the summed
hostRequirements of what runs must fit the host or the pod is refused.
Refusals, in one place
| Condition | Result |
|---|---|
version other than 1 | error |
host neither a reference form nor a valid inline spec | error |
neither repos nor discover listing anything, or the same repository twice (either spelling) | error |
url not a repository URL or owner/name | error |
ref with whitespace, .., a leading -//, or a .lock suffix | error |
devcontainer absolute, climbing (..), or not .json | error |
access missing, or access.github missing / with neither org nor users | error |
access.tailscale.hostname with an unknown placeholder or a dot | error |
sync.schedule not five cron fields | error |
| any unknown key at any level | error |
| a repository with no dev-container file at its ref (online) | error |
a ref that cannot be resolved (online) | error |
--offline | warning — nothing fetched; a repo: host also skipped |
devcontainer: false or pod.sshPort without a top-level pod: | error — a workspace or a port means nothing without a pod |
pod.hostnames.* without {user}, with {repo}, or not ending in .{zone}; port without {port} | error |
pod.tailscale.hostname without {user}, or with a dot | error |
pod.default not listed, or naming a workspace (devcontainer: false, pod.start: clone) or an on-demand entry | error |
pod.start: on-demand/clone on a devcontainer: false entry, or pod.start without a top-level pod: | error |
pod.maxMembers smaller than the number of auto entries (or, at provision, of what would run) | error — lists the entries |
{repo} in pod.hostnames.port while portScope is shared; {repo} in pod.hostnames.default always | error |
(online) portScope: member and two members’ ports rendering to one hostname | error |
(online) pod.enforceBudget on and the members’ summed hostRequirements over the host | error |
| two container entries on one ssh port (a pin colliding with a positional default counts) | error |
pod.sshPort = 22 (the front door) | error |
(online) pod.sshPort = 5432, 6379, 8079 or 9000 (the built-in services) | error — one database, one cache, one object store per pod |
in a pod fleet, per-entry access.tailscale | warning — ignored; the pod has one address |
(online) services.postgres, services.redis or services.s3 in two members | error — one database, one cache, one object store per pod |
| (online) a port number declared by two members, or equal to a member’s ssh port | error — every declared port is a port of the whole pod |
| (online) a public port name declared by two members | error — its hostname routes for every member at once |
(online) a routed port left on the default auth (the sign-in) on a host with no gateway: block | error — it would answer anyone; add the block, or auth: none on the port |
(online) a member’s name reserved (net, redis, redis-rest, s3, s3-init, routes, egress), all digits, or shared with another member or workspace | error — customizations.replicator.repo.name renames it |
(online) a workspace named like a file of the shared root (agents.md, .claude, …) | error — it would be mounted over that file |
(online) pod.siblings on and two members declaring different remoteUsers | error — one user is what makes a shared tree safe |
| (online) a member’s name equal to any member’s public port name | error — one label space in the pod’s routes |
(online) a member’s file declaring hostnames different from the pod’s | error — the pod’s templates name every member |
(online) a member declaring the standard docker-in-docker feature | warning — unavailable in a pod; the member runs without it |
(online) the members’ summed hostRequirements over the host | warning |