Template reference
.ciresk/dev.yaml tells Ciresk dev what a session runs: the toolchains, the steps that prepare the workspace, the services and their ports, the browser, what may leave the session and how long it lives when nobody uses it. This page is the reference for every field the parser accepts.
Example
Adapted from the template Ciresk runs its own monorepo with. Replace registry.example.com with the registry your images come from.
version: 1
templates:
app:
description: the web app on postgres 17 with pgvector; chromium on demand
toolchains: {node: "24", pnpm: "11.1.3"}
workspace:
prepare:
- name: node
run: [pnpm, install, --frozen-lockfile, --prefer-offline]
memory: 3Gi
cpus: 2.0
inputs: [pnpm-lock.yaml, pnpm-workspace.yaml, "**/package.json"]
shared: true
services:
postgres:
image: registry.example.com/mirror/pgvector/pgvector:0.8.6-pg17@sha256:cf134a767f474095eeba57e0117be8e568e011a63f33fbf252f14c9b760f8e6f
command: [postgres, -c, shared_buffers=64MB, -c, max_connections=100]
env: {POSTGRES_USER: app, POSTGRES_PASSWORD: app, POSTGRES_DB: app, PGDATA: /var/lib/postgresql/data/pgdata}
ports: [{name: postgres, port: 55437, target: 5432, kind: tcp}]
readiness: {exec: [pg_isready, -h, 127.0.0.1, -U, app, -d, app]}
volumes: [{name: postgres, path: /var/lib/postgresql/data}]
memory: 256Mi
cpus: 0.25
web:
image: workspace
after: [node:completed]
start: on_demand
idle_stop: 30m
command: [pnpm, dev]
env: {NEXT_TELEMETRY_DISABLED: "1", DATABASE_URL: "postgres://app:[email protected]:5432/app"}
ports: [{name: web, port: 3000, kind: http}]
readiness: {http: {port: 3000, path: /}}
memory: 1Gi
memory_ceiling: 1792Mi
memory_max: 2560Mi
cpus: 1.0
cache_dirs: [.next, node_modules/.cache]
cache_max: 4Gi
heal: {restart_on: [memory_ceiling, memory_growth, not_answering], growth_factor: 3}
serves: static
diagnostics: [{next_mcp: 3000}]
browser:
image: registry.example.com/mirror/playwright/mcp:v0.0.83@sha256:690763cbb437ee7233f538a953bb5390bf03791eb5c965b471e59d17a6132fcb
start_url: "{origin:3000}"
egress: []
registry_cache: true
idle: 60m
disk: 32Gi
shell: {memory: 6Gi, cpus: 4.0}ciresk dev up app installs the node packages once, starts postgres, and starts web the first time something opens http://app.localhost:3000. web declares serves: static because this app writes no generation marker, so ciresk dev wait --gen does not wait for it (the warning workspace_serves_static).
Rules for the whole file
| Rule | What it means |
|---|---|
| Location | .ciresk/dev.yaml at the root of the repository. ciresk dev sends its bytes and their digest with every up, so the session runs exactly the file in your worktree. |
| Size | At most 256 KiB. |
version | Required; 1 is the only version. |
templates | Required; a map of named templates. One file holds as many as you need, for example one per product of a monorepo. |
| Names | Template, service, prepare step, port, volume, file and identity names match ^[a-z][a-z0-9-]{0,30}$. |
| Unknown fields are errors | A typo is refused with its path, never silently ignored. |
| Sizes | Bytes as a whole number, or a whole number with Ki, Mi, Gi, Ti (binary) or k, M, G, T (decimal): 768Mi, 8Gi. No fractions. |
| Durations | A whole number with ms, s, m or h: 500ms, 30m, 2h. |
| CPUs | A decimal above 0 and at most 256: 0.25, 1.0. |
| Validation | ciresk dev up checks the template on your machine before it sends it, and the control plane checks it again with your organisation's policy. Any error refuses the session with exit 2 template_invalid and the list of findings, each with its path, code and, where one exists, the command that fixes it. Warnings are reported and never refuse. |
Which template ciresk dev up starts
ciresk dev up <name> starts the named template. Without a name:
A template whose description starts with blocked: is skipped in step 2. When only blocked templates claim the directory, the template named like the repository starts instead, and up says so; when there is none, the blocked one is picked and refused. Starting a blocked template, by name or this way, fails with exit 2 template_invalid and the finding template_blocked. Write blocked: <reason>; next: <what unblocks it> to keep a template in the file before it can run; the text after next: is shown as the next step.
Each worktree gets its own session. When you change .ciresk/dev.yaml under a running session, the session keeps the template it started with and ciresk dev status says the file changed; the next ciresk dev up starts the session again on the new file. sessions.md covers the rest of a session's life.
ciresk dev init
ciresk dev init --from docker-compose.yml --name app
ciresk dev init --from .devcontainer/devcontainer.json --writeinit translates a compose file or a devcontainer into a template and prints it with what it could not translate. Without --from it looks for .devcontainer/devcontainer.json, compose.yaml and docker-compose.yml, in that order. --name names the template (default: the repository's folder), --app <dir> adds an app of a monorepo with its own install step and dev server, --write merges the result into .ciresk/dev.yaml, and --force replaces a template of the same name. Any error finding exits 2 with template_invalid. The findings you will meet most:
| Finding | Meaning |
|---|---|
image_unpinned | The compose image has no digest. init never asks a registry; pin it yourself. |
build_not_supported | The service is built by compose. A session never builds an image: publish it to a registry and pin it. |
host_mount | A bind mount outside the repository has no equivalent in a session. |
secret_in_env | A value looks like a real credential. It is not written to the template. |
port_collision | Two services listen on the same port, which one session cannot hold. |
resources_estimated | The compose file names no resources; init wrote estimates. Check them. |
Template fields
| Field | Type | Default | What it does |
|---|---|---|---|
description | string | empty | One line shown in listings; blocked: marks the template as not runnable (above). |
toolchains | map of name to version | none | The tools installed in every image: workspace container. |
workspace | object | required | Where the code is mounted and the steps that prepare it. |
services | map of name to service | none | The containers of the session. |
browser | object | none | A Chromium the session's agents drive, on the session's own pages. |
egress | list of rows | [] | The destinations outside the session it may reach. Empty means none. |
registry_cache | boolean | false | Gives the session the worker's read-only package registry cache. |
idle | duration | 60m | How long the session lives without activity. 5m to 8h. |
disk | size | required | The session's disk quota, above 0. |
data | object | none | The volumes ciresk dev reset restores. |
shell | object | see below | The SSH and exec containers every session gets. |
ide | object | see below | Editor settings for those containers. |
queue | object | from the session's CPUs | How many operations run at once. |
parameters | list of parameters | none | Typed inputs a session is started with (below). |
presets | list of presets | none | Named sets of parameter values. |
oauth | object | none | Redirect URIs and identity provider hosts for an app's sign-in. Not available yet: the parser accepts it so a template can carry it, and it has no effect. Fields callback_path (string, required), redirect_uris, idp_hosts and start_paths (lists of strings, default empty). |
Toolchains
toolchains: {rust: "1.97.1", node: "24", pnpm: "11.1.3"}| Rule | What it means |
|---|---|
| Names | bun, cargo:cargo-nextest, deno, go, java, node, pnpm, python, ruby, rust, terraform, uv. Anything else is refused with toolchain_refused; install other tools in a prepare step. |
| Versions | A plain version, 1 to 64 characters of letters, digits, ., + and -: "24", "1.97.1". |
| Where | Installed on the worker and mounted read-only into every image: workspace container, which runs the session base image (a Linux image with watchexec and a C toolchain, user 1000, working directory /workspace). |
| Rust | With a rust toolchain every workspace container gets its own CARGO_HOME on a session volume, so cargo install works. Targets and components named in the repository's rust-toolchain.toml are added to the toolchain. |
Workspace
| Field | Type | Default | What it does |
|---|---|---|---|
mount | absolute path | /workspace | Where the code is mounted in every container that has access to it. |
write | stage_rename or in_place | stage_rename | How a synced file is written. in_place keeps the file's inode, for watchers bound to inodes such as node --watch. |
prepare | list of steps | none | Commands that run to completion before the services that wait for them. |
materialise | full or lazy | full | lazy fetches file content on first read instead of before the start. It takes effect only when ciresk dev up --lazy also asks for it. |
prefetch | list of globs | none | Paths fetched first in a lazy session, and the paths that pick this template for a bare ciresk dev up. |
Prepare steps
Each step is a container over the workspace that runs once. Services wait for it with after: [<step>:completed], so a service waits only for the steps it needs.
| Field | Type | Default | What it does |
|---|---|---|---|
name | name | required | The step's name, unique among steps and services. |
run | list of strings | required | The command, as an argument list. Not empty. |
after | list of step names | none | Other steps that must complete first. |
memory | size | required | Memory the step reserves, above 0. |
cpus | decimal | required | CPU share the step reserves, above 0. |
inputs | list of globs | none | Workspace files the step reads. When a synced change touches one, the step runs again. |
shared | boolean | false | Reuses the step's result across sessions whose inputs match. |
layer_inputs | list of globs | none | Only with shared: more files the shared result is built from and keyed by, without re-running the step when they change, such as a Rust build's sources. |
Globs are relative to the workspace and use **, * and ?; .., braces and character classes are refused (bad_glob).
A shared step must list every file it reads in inputs (shared_without_inputs) and wait for no other step (shared_after). The first session of a set of input files on a worker runs the step and, beside it, builds a layer from exactly those files. A later session on that worker whose tracked input files match byte for byte mounts the layer read-only and skips the step: a pnpm install of an unchanged lockfile takes seconds instead of a minute. A Rust build step can be shared too: list Cargo.lock and the manifests in inputs and the sources in layer_inputs; a later session's files that match the build's are marked older than it, so cargo finds the whole build fresh and compiles only the crates whose files differ. Layers stay within your organisation, and a lazy session always runs the step.
Services
services:
api:
image: workspace
after: [rust:completed, postgres]
command: [watchexec, --restart, --exts=rs, --, cargo, run]
ports: [{name: api, port: 8080, kind: http}]
readiness: {http: {port: 8080, path: /ready, status: 200}}
memory: 384Mi
cpus: 0.5Every service and step of a session shares one network namespace: services reach each other on 127.0.0.1 and the in-pod port.
| Field | Type | Default | What it does |
|---|---|---|---|
image | string | required | workspace (the session base image with the template's toolchains), or an image pinned by digest. |
pull | {secret, user} | none | Pulls a pinned image from your own registry with the organisation's secret secret and the registry user user. See Images. |
workspace | none, read_only or read_write | read_write for image: workspace, else none | The service's access to the workspace mount. |
command | list of strings | the image's own | The command, as an argument list. Required for image: workspace. |
env | map of string to string | none | Environment variables. Values may hold placeholders. |
after | list | none | <name> (or <name>:ready) waits until that service is ready; <name>:completed waits until that step or run: once service has succeeded. |
run | service or once | service | once runs to completion, like a prepare step with its own image (a migration, a bucket setup). |
restart | on-failure or never | on-failure for services, never for once | on-failure restarts a service that fails, at most 3 times. |
start | eager or on_demand | eager | on_demand starts the service the first time something reaches its port, or ciresk dev exec or ciresk dev restart names it. |
idle_stop | duration | none | For an on_demand service: stop it after this long without a connection to its ports. Above 0. |
ports | list | none | The ports it listens on. |
readiness | object | none | Exactly one of tcp: <port>, http: {port, path, status} or exec: [argv]. An http probe without status accepts any answer below 500. Without a probe the service is ready once it runs. |
memory | size | required | Memory the service reserves; the session is placed on a worker by the sum. Above 0. |
memory_ceiling | size | 1.5 x memory, at most memory_max | Where the kernel starts to throttle the service, and the memory_ceiling heal trigger. |
memory_max | size | 2 x memory | The hard limit; above it the process is killed. memory at most memory_ceiling at most memory_max, or memory_order. |
cpus | decimal | required | The service's CPU share, above 0. A share, not a cap: a process still sees every core of the host, so limit a build's parallelism yourself (cargo build -j 4). |
shm | size | the runtime's | Size of /dev/shm. Above 0 when set. |
volumes | list of {name, path} | none | Named volumes at absolute paths. They live as long as the session; services that name the same volume share it. |
files | list of {name, path, content} | none | Files written for the service at absolute paths outside the workspace: a config file for an upstream image. |
cache_dirs | list of workspace paths | none | Directories the service fills as a cache (.next, target). |
cache_max | size | none | When cache_dirs grow past it, the service is reported degraded. Needs cache_dirs. |
serves | hmr, restart, diagnostics or static | restart for image: workspace, else static | How the service proves it runs your latest change. |
hmr_probe | object | none | Measures hot reload for serves: hmr. |
diagnostics | list | none | Where compile and type errors come from. |
heal | object | none | Restarts the service when a measured condition holds. |
share | per_session or per_worker | per_session | per_worker (one server per worker shared by many sessions) is not available yet and is refused with share_pending_owner. |
sealed | boolean | false | Seals the container: no exec, SSH or log stream into it, and no other container may share its volumes. Needs a pinned image. |
Images
An image other than workspace is named by its registry host and pinned by digest: registry.example.com/team/api:1.4@sha256:<64 hex>. A short name such as postgres:17 is refused with unqualified_image, an image without a digest with unpinned_image; both findings name the fully qualified form to pin. A digest of all zeros is a placeholder for an image not released yet and is refused with unreleased_image.
An image on Ciresk's own registry (where the public images a template uses are mirrored under the same tag and digest) is pulled as it always was. An image on any other registry is yours: GHCR, Docker Hub, ECR, Artifactory, Harbor or any OCI registry. The worker pulls it before each start of its service, outside the session, through the session's own egress:
- The registry's host, and every host it redirects to, must be in the template's
egress, in your organisation's egress ceiling when it has one, and allowed by the installation'segress.yaml, as for any other egress. A host that is not is refused withimage_registry_not_allowed;ciresk dev egressnames each refused redirect. Common sets: GHCRghcr.ioandpkg-containers.githubusercontent.com; Docker Hubregistry-1.docker.io,auth.docker.ioandproduction.cloudflare.docker.com. - A private image names its credential with
pull: {secret: <name>, user: <registry user>}. The secret is an organisation secret (ciresk dev secrets set <name> --host <registry> --repo <repository>, with a read-only token as its value);dev uprefuses a secret the session is not granted withpull_secret_not_granted. Bind the secret to the host the registry serves from:registry-1.docker.iofor Docker Hub, which also sends the credential toauth.docker.io, so bind both there. The value never enters the session, nor the pull itself: the pull holds the session's placeholder, and the worker's proxy puts the value in the request only when it goes to a host the secret is bound to, so a registry that sends the credential to any other host gets the placeholder. The secret keeps the default header,authorization(pull_secret_headerotherwise); a token endpoint on a host the secret is not bound to fails the service withpull_realm_not_bound, naming the host to add.
services:
api:
image: ghcr.io/acme/api:1.4@sha256:<64 hex>
pull: {secret: ghcr, user: acme-bot}
memory: 256Mi
cpus: 0.5
egress:
- {host: ghcr.io, ports: [443], purpose: the api image}
- {host: pkg-containers.githubusercontent.com, ports: [443], purpose: the api image's layers}Environment
Values are plain strings; quote numbers and booleans (PORT: "3000"). Four placeholders are filled in for each session:
| Placeholder | Value |
|---|---|
{label} | The session's label: the template name in your main worktree (app), the template name and the worktree's folder in another (app-feat-x), with -2, -3 added when it is taken on your machine. |
{origin:<port>} | http://<label>.localhost:<port>. The port must be an http port of the template whose in-pod target is the same port. |
{random:<name>} | 24 random letters and digits, the same for one name within a session, never logged. The name matches ^[a-z][a-z0-9_-]{0,30}$. |
{param:<name>} | The session's value of a parameter the template declares (see "Parameters and presets"). Only in env. |
Any other {...} group is refused with unknown_placeholder.
Ciresk sets CIRESK_LOGIN_KEY, CIRESK_LOGIN_TTL, CIRESK_SESSION_EPOCH and the model variables ANTHROPIC_BASE_URL, OPENAI_BASE_URL, ANTHROPIC_API_KEY, OPENAI_API_KEY, ANTHROPIC_MODEL, NODE_EXTRA_CA_CERTS, SSL_CERT_FILE and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, REQUESTS_CA_BUNDLE and every variable whose name starts with CIRESK_SECRET_ (in any letter case) itself; a template that sets one is refused with reserved_env.
Placeholders are filled in only in a service's env, in the browser's start_url, and in a test identity's http login or revoke call (its URL, headers and body). {param:<name>} is refused in a URL (parameter_env_only). Anywhere else (a command, a file's content, a probe) the text is passed as it is.
The template is stored with every session and readable by anyone who can read the repository. Never put a real credential in env: use local stand-ins (POSTGRES_PASSWORD: app) or {random:<name>}. A session holds no reusable secret.
With registry_cache: true, every workspace container gets npm_config_registry and pnpm_config_registry pointing at the cache, unless the template sets them.
On-demand services
A request through the service's URL, a local port or an SSH forward starts an on_demand service when it reaches a port the service owns, and waits for it to be ready. A service owns a port through its tcp or http readiness probe or its hmr_probe. With only an exec probe it owns none, so a request never starts it (the warning on_demand_owns_no_port).
An on_demand service with idle_stop stops after that long with no connection to its ports; the next request starts it again. An eager service cannot wait for an on-demand one (after_on_demand), and a run: once service is never on demand (once_on_demand).
Serving the latest change
Every sync of your worktree is a numbered generation. serves tells Ciresk how a service shows which generation it runs, so ciresk dev status can show it and ciresk dev wait --gen <n> can wait for it. Only image: workspace services may use anything but static.
| Value | How the service proves it |
|---|---|
restart | The process writes the generation to /run/ciresk/diag/<service>.gen and then becomes ready. Copy <mount>/.ciresk/generation there at the start of each rebuild. |
hmr | The hmr_probe writes a nonce to a workspace file and fetches it back through the dev server. Needs hmr_probe. |
diagnostics | A type checker in watch mode finished a cycle over the generation. Needs a tsc_json diagnostics source. |
static | Always current: databases, caches and upstream images. |
hmr_probe fields: file (a workspace path the probe writes), content (holds {nonce} exactly once), path (the URL path that returns the file, starting with /), port (an http port of this service), slo (default 2s) and every (default 1m, at least slo).
diagnostics is a list; each entry is exactly one of:
| Source | What it reads |
|---|---|
cargo_json: <file> | cargo build --message-format json output, in a file under /run/ciresk/diag/. image: workspace only. |
tsc_json: <file> | TypeScript errors written by ciresk-tsc-json (in the session base image), in a file under /run/ciresk/diag/. image: workspace only. |
next_mcp: <port> | Next.js's own MCP endpoint on an http port of this service. |
ciresk dev diagnostics returns them as data.
Heal
| Field | Type | Default | What it does |
|---|---|---|---|
restart_on | list | none | Restarts the service when one of these holds: memory_ceiling (at memory_ceiling for 30 s), memory_growth (more than growth_factor times its baseline for 2 minutes), hmr_failing (needs hmr_probe), not_answering (needs readiness or hmr_probe). |
growth_factor | decimal | 3 | The memory_growth threshold, at least 1.5. |
Without heal, these conditions only mark the service degraded in ciresk dev status. A ready service that fails its readiness probe three times in a row is degraded either way.
Ports and URLs
ports:
- {name: web, port: 3000, kind: http}
- {name: postgres, port: 55437, target: 5432, kind: tcp}| Field | Type | Default | What it does |
|---|---|---|---|
name | name | required | Unique across the whole template. |
port | 1 to 65535 | required | The port of the URL for an http port; for a tcp port, the number the local port is derived from (below). |
target | 1 to 65535 | port | The port the service listens on inside the session. |
kind | http, tcp or internal | required | How the port is reached. |
| Kind | Reached at |
|---|---|
http | http://<label>.localhost:<port> from your browser, curl and your programs. The per-machine daemon serves every session on the same port and tells them apart by the Host header, so two worktrees on port 3000 are two origins. While a service starts, the URL answers 503 with Retry-After. |
tcp | A loopback port of its own for each session, between 20000 and 29999, derived from the worktree and port, so it stays the same on every run. ciresk dev status prints it with its target; point your database client at 127.0.0.1:<local>. |
internal | Never on your machine; only Ciresk's own services in the session reach it. Never has an origin. |
Each in-pod port is used once per session (duplicate_target_port) and each port of an http or tcp port once per template (duplicate_port). Ports 3128, 3129, 3130 and 3131 belong to the session itself, and 8931 too when the template has a browser (reserved_port). On Windows, run ciresk dev setup --resolver once so <label>.localhost resolves for every program; see quickstart.md.
Egress
Nothing leaves a session by default. Each row asks for one destination:
egress:
- {host: api.stripe-mock.example.com, ports: [443], purpose: the payments sandbox the api calls, required: true}
- {host: "*.tiles.example.org", ports: [443], purpose: map tiles in the web app}| Field | Type | Default | What it does |
|---|---|---|---|
host | DNS name | required | A host name, or *. and a name for its subdomains. No IP address, scheme or port (egress_host_not_name). At most 253 characters, labels at most 63. |
ports | list of ports | required | At least one, none of them 0 (egress_ports). |
purpose | string | required | Why the session needs it. |
required | boolean | false | A required row the session cannot have refuses ciresk dev up; an optional one is dropped and the session starts without it. |
At ciresk dev up each row passes these checks in order; the session gets what passes all of them:
A required row that fails a check refuses the session with exit 4 egress_denied. The answer lists the refused destinations as host:port in denied and names the check in reason:
reason | Refused by | Fix |
|---|---|---|
vendor | Check 2 | Remove the row; a session reaches models and tools only through Ciresk's own paths. |
org_policy | Check 3 | Ask an admin to cover the host (ciresk dev policy show, admin.md), or make the row optional. |
agent_policy | Check 4 | Ask an admin to list the host for agents. |
| (none) | Check 5 | Ask the installation's operator, or make the row optional. |
The session's egress decisions are recorded every minute per host, port and the rule that allowed or refused them, and ciresk dev egress lists them with the change that would let a refused one out. See security.md for the network boundary.
Registry cache
registry_cache: true gives the session the worker's read-only cache of the public npm and crates.io registries, so pnpm install and cargo build fetch packages without an egress row. It is not an egress row and needs none. Every template that installs packages without a private registry should turn it on.
Browser
browser:
image: registry.example.com/mirror/playwright/mcp:v0.0.83@sha256:690763cbb437ee7233f538a953bb5390bf03791eb5c965b471e59d17a6132fcb
start_url: "{origin:3000}"
idle_stop: 10m
identities:
- {name: nora, login: none, cookie_max_secs: 0, revoke: none}The browser is a Chromium in the session behind the Playwright MCP server, started the first time a client uses it. Claude Code reaches it through ciresk dev mcp browser; ciresk dev evidence captures a page from it.
| Field | Type | Default | What it does |
|---|---|---|---|
image | pinned image | required | The upstream Playwright MCP image, pinned by digest. |
start_url | string | required | The first page. Starts with {origin:<port>}. |
idle_stop | duration | 10m | Stops the browser after this long unused. Above 0. |
contexts | integer | 4 | Concurrent browser contexts, one per client, at least 1. The limit is not enforced yet: a further client is not refused. |
identities | list | none | Test users the browser can log in as. |
The browser reaches only the session's own origins; a start_url or login URL elsewhere is refused with browser_open_web_phase_2. Opening other sites from the session's browser is not available yet. A template with a browser cannot name a service browser.
Test identities
| Field | Type | Default | What it does |
|---|---|---|---|
name | name | required | What ciresk dev login <identity> --cookies or --revoke names. |
login | see below | required | How the identity logs in. |
cookie_max_secs | integer | required | The longest the login cookie may live, at most the template's idle in seconds. 0 for login: none. |
revoke | none or {http: {method, url, headers, body}} | required | How a login is revoked. Ciresk sends the call, with the same rules as an http login, when you run ciresk dev login <identity> --revoke and before the session stops. With none, a login ends when its cookie expires, and --revoke answers identity_without_revoke; for a login: none identity there is nothing to revoke and it succeeds. |
login | What happens |
|---|---|
none | The app needs no login, for example a preview mode with fixtures. |
{route: /__ciresk/<path>, port: <port>} | The app's own dev-only login route on an http port of the session. Ciresk mints a one-time token signed with the app's CIRESK_LOGIN_KEY, which it sets in the service that serves that port together with CIRESK_LOGIN_TTL (the identity's cookie_max_secs), and the browser opens the route with it. Paths under /__ciresk/ never reach the app through a URL from outside the session. cookie_max_secs is above 0 and the same for every identity on one port. |
{http: {method, url, headers, body}} | A login request Ciresk sends inside the session to a URL that starts with {origin:<port>}, with the header x-ciresk-cookie-max-secs set to the identity's cookie_max_secs. A Set-Cookie that outlives it fails the login with identity_cookie_over_bound. ciresk dev login <identity> --cookies answers the cookies; ciresk_login in ciresk dev mcp browser sets them in that client's own browser context and opens the origin. method is GET, POST, PUT, PATCH or DELETE (bad_method). A header name that is not letters, digits and -, one Ciresk sets itself (Host, Connection, Content-Length, Transfer-Encoding), or a value with a control character other than a tab, is refused with bad_header; a URL with a control character with bad_url. A login that answers an error status fails with login_failed. A literal Authorization, Cookie or Proxy-Authorization header is refused with literal_credential; a secret placeholder is refused with identity_needs_egress_secrets; secret placeholders in a login are not available yet. |
{script: <path>} | A Playwright script in the workspace. Not available yet: it is refused with login_script_later; use a login route or an http login. |
Shell and editors
Every session gets two containers on the session base image: shell (read-write) and shell-ro (read-only). ciresk dev exec and ciresk dev ssh run in shell, ciresk dev ssh --read-only in shell-ro, and ciresk dev code opens VS Code, Cursor or Windsurf over Remote-SSH with its editor server in shell (JetBrains Gateway likewise; see editors.md). They start with the first SSH channel or exec and stop idle_stop after the last one closes. The names shell, shell-ro, ide and ide-ro are reserved (reserved_name).
| Field | Type | Default | What it does |
|---|---|---|---|
shell.memory | size | 2Gi | Memory of each shell container. |
shell.cpus | decimal | 1.0 | CPU share of each shell container. |
shell.idle_stop | duration | 30m | How long the shells stay up after the last channel closes. Above 0. |
ide.exclude | list of globs | none | Joins the editor's built-in watcher and search excludes (**/target/**, **/node_modules/**, **/.next/**, .ciresk/**). |
ide.memory | size | none | When set, sizes shell instead of shell.memory, room for the editor server. |
ide.image | image by digest | Ciresk's | VS Code in the browser runs this image instead of Ciresk's: your organisation's build FROM ciresk/dev-ide with its extensions built in, so the digest fixes every extension's bytes. Pinned like a service's image; your organisation can lock it. |
ide.gallery | https url | none | Your own Open VSX mirror, from which people may install more extensions in the read-write browser editor for the session's life. An https://host[:port][/path] URL with no query, fragment or credentials (ide_gallery_url). Refused unless a row of egress reaches its host and port (ide_gallery_not_egress), and for Microsoft's Marketplace (ide_gallery_microsoft). |
Size shell for what you run with exec: a cargo build of a large workspace needs more than the 2Gi default. VS Code in the browser runs in containers of its own (ide and ide-ro) when your organisation turns it on; see editors.md.
Seed data
data:
volumes: [postgres]
seed: [postgres-setup]
inputs: [migrations/**]ciresk dev reset puts the named volumes back to the state they had when the seed jobs completed, in about a second. --volume resets only some, --rebuild empties them and runs the seed jobs again.
| Field | Type | Default | What it does |
|---|---|---|---|
volumes | list of volume names | required | Volumes declared by a service of the template; at least one. |
seed | list | none | Prepare steps or run: once services; the seed is taken when every one has completed. |
inputs | list of globs | none | Files whose change marks the seed stale, such as migrations. |
Queue
| Field | Type | Default | What it does |
|---|---|---|---|
queue.slots | integer | the session's CPUs, at least 2 | Operations (exec and similar) that run at once; more wait in a fair queue. At least 1. |
queue.per_principal | integer | half of slots, at least 1 | How many of them one person or agent may hold. At least 1, at most slots. |
Idle stop and disk
A session is active while your worktree syncs to it, a stream (a URL, a port, SSH) is open, or a call names it. After idle without activity it stops: a session on your worktree ends and its volumes are deleted, while your code is safe in your worktree; a cloud session is paused with its checkpoint unless your organisation set automatic stops to end sessions (sessions.md). A session that gets no first sync within 60 seconds of starting stops too.
The template's idle is 5m to 8h (idle_out_of_range). Your organisation narrows the range with an idle minimum and maximum, and may set a default that applies instead of the template's:
| Case | Result |
|---|---|
The template names an idle over the organisation's maximum | Refused at ciresk dev up with template_invalid and the finding idle_over_policy, which names the largest value allowed. An idle of exactly 60m counts as the default and is never refused. |
| The organisation sets a default idle | It applies instead of the template's. |
| Otherwise | The template's idle (60 minutes when it names none) is moved into the organisation's range. |
ciresk dev up --idle 2h | Your own choice for this session; outside the range it is refused with exit 4 idle_out_of_bounds, which names both bounds. |
A remote session (--remote) without --idle, when the organisation's automatic stops end sessions | The organisation's maximum, since the session holds the only copy of its edits. Under the default, where automatic stops pause a cloud session, it gets the same idle as any session. |
disk is the session's quota for its writes, volumes and caches. Your organisation may also cap the memory and CPUs one session reserves in total; a template over the cap is refused with exit 4 session_too_large.
Parameters and presets
A template can take inputs, so one template serves several ways of working: with or without demo data, with more
or fewer workers. Each parameter has a type and a default, and reaches the stack only through {param:<name>} in a
service's env: a parameter cannot change a size, an image or which services run.
parameters:
- {name: seed, type: choice, options: [none, demo, full], default: demo, description: Data the database starts with}
- {name: api-workers, type: number, default: 2, min: 1, max: 8, mutable: true}
- {name: clickhouse, type: bool, default: true, mutable: true}
presets:
- {name: light, description: No data and no ClickHouse, values: {seed: none, clickhouse: false}}
services:
api:
env: {SEED: "{param:seed}", API_WORKERS: "{param:api-workers}", CLICKHOUSE: "{param:clickhouse}"}| Field | Type | Default | What it does |
|---|---|---|---|
name | name | required | How --param, presets and {param:<name>} refer to it. |
type | string, number, bool, choice | required | A string of at most 256 characters with no control characters, a whole number, true or false, or one of options. |
default | value | required | The value when nobody chooses one. It must pass its own type. |
options | list | required for choice | choice only: 1 to 64 distinct values of at most 256 characters each. |
min, max | whole numbers | none | number only. Either may be left out. |
mutable | boolean | false | Whether ciresk dev up with another value starts your running session again with it. An immutable value takes --recreate. |
description | string | empty | Shown in the console's form. |
The parameters are a list, and the console's form shows them in that order. Values in the file (default, options and preset values) are written as YAML strings, whole numbers or booleans; every value reaches the session as a string, such as "2" or "true".
A preset sets some parameters; the rest keep their defaults.
| Preset field | Type | Default | What it does |
|---|---|---|---|
name | name | required | What --preset names. |
description | string | empty | Shown in the console's preset picker. |
values | map of parameter name to value | none | The values it sets. Each names a declared parameter and passes its type. |
A template has at most 32 parameters and 16 presets. No preset applies unless you choose one: there is no default preset.
ciresk dev up kepler --preset light --param api-workers=4The value of a parameter is its default, then the preset's, then --param's. --param <name>=<value> is repeatable and splits at the first =, so a value may hold one. An agent passes the same as parameters and preset to the dev_session_up MCP tool. A preset or parameter the template does not declare, or a value its type refuses, is refused with exit 2 parameter_invalid, and each finding says what the parameter takes.
The session records the values it runs with: ciresk dev status, the API, the console and the audit trail show them. The console's "New session" form shows the parameters and presets of the version promoted for the repository, with a preset picker that fills the fields. Your organisation can lock images, egress and container sizes over a template (Locked fields), but not parameters or presets.
Parameter values are visible to everyone who can see the session. Never put a secret in a parameter. A
parameter whose name contains secret, token, password, passwd, credential, apikey, api-key or
private-key is refused with parameter_secret. That rule catches the common names only; a secret belongs in
your organisation's secrets (ciresk dev secrets set <name>).
With a session running, ciresk dev up compares only the parameters you name, directly or through --preset.
Naming none returns the running session as it is. A different mutable value ends the session and starts a new one
with it, on the same label; its volumes start fresh. A different immutable value is refused with
parameter_immutable until you add --recreate. Agents using dev_session_up get parameters_changed or
parameter_immutable and pass recreate: true to apply the change. ciresk dev start keeps the values a paused
session was created with.
Template versions
An organisation can publish a template as a version, promote a version for one repository or for all of them, and require that every new session runs a promoted version.
ciresk dev templates publish app
ciresk dev templates list
ciresk dev templates promote eu1_devtv_0192... --repo github.com/acme/app
ciresk dev templates promote eu1_devtv_0192... --all| Command | What it does |
|---|---|
publish [<template>] | Validates the template of this worktree's .ciresk/dev.yaml with the same rules up applies and records it as a version (eu1_devtv_...) with the images it pins. Without a name it publishes the template ciresk dev up would start here. Publishing the same name and file again answers the existing version. Needs dev:templates.write; a person or a service key, never an agent. |
list [--template <name>] | The organisation's versions, newest first: template, digest, id and the repositories each is active for. Needs dev:templates.read; agents read it with the dev_templates_list MCP tool. |
promote <version_id> --repo <url> or --all | Makes the version the active one of its template for one repository (a remote URL in any form git accepts, or host/owner/name) or for every repository that has no promotion of its own. It replaces the version active before, and both are recorded. Needs dev:templates.write, and only a person can promote. |
A version is a template name plus the digest of the whole .ciresk/dev.yaml, so a change to any template in the file is a new version of every template in it. There is no demote: promote another version, or set the policy back to any.
Requiring a published version
The organisation policy's templates setting is any (the default: every repository's own file runs) or published. Admins change it with ciresk dev policy set; see admin.md.
Under published, a new session (ciresk dev up or ciresk dev up --recreate) starts only when its template name and dev-file digest are the version promoted for its repository; the repository's own promotion wins over --all. Otherwise it is refused with exit 4 template_not_published, whose next step is ciresk dev templates publish <template>; when another version is promoted, the answer names it in promoted (version_id, template_digest, repository). A running session keeps running, and ciresk dev up still returns it. Every edit of .ciresk/dev.yaml therefore needs a publish and a promote before the next new session.
A remote session started from the console has no worktree and no file of its own, so it always runs the version promoted for its repository, under either setting; with none promoted it is refused with template_not_published. ciresk dev up --remote sends your worktree's file like any other up.
Locked fields
An organisation can also lock a few fields over every repository's template: an image per registry repository (a template's image of that repository gets the locked tag and digest), the egress rows, and a cap on every container's memory and CPUs (larger values are lowered to the cap, not refused). The overlay applies before validation, so a locked template passes the same checks. Everything else stays the repository's. ciresk dev policy show shows what is locked; see admin.md.
Validation findings
Errors refuse ciresk dev up and publish with exit 2 template_invalid. Paths in a finding are relative to the template: services.web.ports[0].
| Finding | Meaning | Fix |
|---|---|---|
yaml, unsupported_version | The file is not valid YAML, has an unknown field or a wrong shape, or version is not 1. | Fix the field the message names. |
bad_name, bad_quantity, bad_duration, bad_decimal | A name, size, duration or CPU value is off its format. | Use the formats under "Rules for the whole file". |
toolchain_refused | A toolchain name not on the list, or a version that is not plain. | Name a listed tool, or install it in a prepare step. |
unpinned_image, unqualified_image, unreleased_image | An image without a digest, without its registry host, or pinned to the all-zero digest. | Set <host>/<path>:<tag>@sha256:<digest> as the finding's next step says. |
pull_needs_pinned_image, pull_secret_invalid, pull_user_invalid | pull on image: workspace; a secret name that is not 1 to 63 lower-case letters, digits, - and _ starting with a letter or digit, or that starts with model_; a registry user that is not 1 to 255 printable characters without :. | Pin an image of your registry; name the secret as ciresk dev secrets list shows it; fix the user. |
resources_missing, memory_order | memory, cpus or disk missing or 0; or memory, memory_ceiling, memory_max out of order. | Declare positive values with memory at most memory_ceiling at most memory_max. |
empty_command, workspace_image_needs_workspace | A prepare step or image: workspace service without a command, or an image: workspace service with workspace: none. | Add the command; give the service workspace access. |
idle_out_of_range, idle_over_policy | idle outside 5m to 8h, or over your organisation's maximum. | Lower idle; ciresk dev policy show shows the maximum. |
unknown_after, bad_after, after_cycle | after names nothing, has a bad suffix, or the waits form a cycle. | Name an existing step or service as <name>, <name>:ready or <name>:completed. |
after_once_needs_completed, completed_needs_once, after_on_demand | A wait on a step without :completed, :completed on a service, or an eager service waiting on an on-demand one. | Match the suffix to the target; make the waiting service on demand too. |
once_on_demand, idle_stop_needs_on_demand | A run: once service on demand, or idle_stop on an eager service. | Remove start: on_demand or idle_stop. |
duplicate_container_name, reserved_name | Two steps or services share a name (a service named browser in a template with a browser counts), or a name, volume or file is one Ciresk uses: the containers shell, shell-ro, ide and ide-ro; the volumes shell-home, ide-home, ide-run, ide-ro-run and cargo-home; the files ide-settings, ide-web-settings and cargo-config. | Rename it. |
reserved_port, duplicate_target_port, duplicate_port, duplicate_port_name | A port the session uses itself, or a port or port name declared twice. | Move or rename the port. |
unknown_placeholder, origin_port_unknown, origin_not_http, origin_internal_port, origin_target_mismatch | A placeholder that does not exist, or {origin:<port>} without an http port whose port and target are that number. | Declare the port as http with the same port and target. |
reserved_env | env sets a variable Ciresk sets. | Remove it. |
bad_probe, bad_diagnostics | A readiness probe or diagnostics entry with none or more than one of its keys. | Give exactly one. |
serves_needs_workspace_image, hmr_needs_probe, diagnostics_needs_tsc_json | serves other than static on a pinned image, hmr without hmr_probe, or diagnostics without a tsc_json source. | Match serves to what the service has. |
probe_needs_serves_hmr, probe_port_not_own, probe_needs_nonce, probe_timing | An hmr_probe on a service that is not serves: hmr, on a port that is not its own http port, without exactly one {nonce}, or with every under slo. | Fix the probe field the path names. |
diagnostics_needs_workspace_image, diagnostics_outside_mount, diagnostics_port_not_own | A file source on a pinned image or outside /run/ciresk/diag/, or next_mcp on a port that is not this service's http port. | Move the file or the port. |
heal_trigger_unmeasured, growth_factor_low | A heal trigger the service has no probe for, or growth_factor under 1.5. | Add the probe or remove the trigger; raise the factor. |
bad_path, file_in_workspace, cache_max_without_dirs | A path that must be absolute (or workspace-relative) is not, contains .., a generated file lands in the workspace, or cache_max has no cache_dirs. | Fix the path; add cache_dirs. |
bad_glob, shared_without_inputs, shared_after, layer_inputs_not_shared | A glob with .., braces or classes; a shared step without inputs or with after; layer_inputs on a step that is not shared. | Fix the glob; list the step's inputs and drop its after; set shared: true or drop layer_inputs. |
egress_host_not_name, egress_ports | An egress host that is not a DNS name, or a row without ports. | Write the host name and at least one port. |
browser_open_web_phase_2, bad_contexts | A browser URL that does not start at a session origin, or contexts: 0. | Start the URL with {origin:<port>}; use at least 1. |
ide_gallery_url, ide_gallery_microsoft, ide_gallery_not_egress | ide.gallery is not a plain https:// URL, names Microsoft's Marketplace, or has no egress row for its host and port. | Point it at your Open VSX mirror and add its egress row. |
bad_login, bad_revoke, identity_cookie_over_idle, identity_none_has_no_cookie, literal_credential, identity_needs_egress_secrets, bad_method, bad_header, bad_url, login_script_later | An identity's login or revoke has the wrong shape, its cookie outlives idle, login: none with a cookie, a credential in a login header, a method or header Ciresk does not send, or a script login, which is not run yet. | Fix the identity as its row under "Test identities" says. |
login_route_not_filtered, login_port_unknown, login_route_needs_cookie_bound, login_ttl_conflict | A login route outside /__ciresk/, on a port no service serves over http, with cookie_max_secs: 0, or two identities on one port with different cookie lifetimes. | Fix the route, the port or cookie_max_secs. |
data_volumes_empty, data_unknown_volume, data_seed_not_once | data without volumes, a volume no service declares, or a seed that is not a step or run: once service. | Name declared volumes and run-once seeds. |
bad_queue | A queue value of 0, or per_principal over slots. | Use at least 1 and at most slots. |
sealed_needs_image, sealed_volume_shared, share_pending_owner | A sealed service on image: workspace or sharing a volume; share: per_worker. | Pin an image and give it its own volumes; use per_session. |
template_blocked | The template you start has a description that starts with blocked:. | Do what its next step says, then remove the prefix. |
parameter_duplicate, preset_duplicate, parameters_too_many | Two parameters or presets share a name, or more than 32 parameters or 16 presets. | Rename or remove one. |
parameter_default_invalid, parameter_options_invalid, parameter_bounds_invalid, parameter_field_invalid | A default its type refuses; a choice without options, or an empty, repeated or over-long options list; min over max; or options, min or max on a type without them. | Fix the parameter the path names. |
parameter_secret | A parameter's name reads like a secret. | Use an organisation secret: ciresk dev secrets set <name>. |
preset_unknown_parameter, preset_value_invalid | A preset sets a parameter the template does not declare, or a value its type refuses. | Name a declared parameter with a value it takes. |
unknown_parameter, parameter_env_only | {param:<name>} names no parameter, or appears in the browser's start_url or an identity's login URL. | Declare the parameter; use it in a service's env. |
on_demand_owns_no_port, workspace_serves_static | Warnings: an on-demand service no request can start, or workspace code reported as always current. | Give it a tcp or http probe; set serves to what the service proves. |
Other refusals at ciresk dev up
| Code | Exit | Meaning | Fix |
|---|---|---|---|
no_template | 2 | No .ciresk/dev.yaml, no template of that name, or no way to pick one. | Name the template: ciresk dev up <name>. |
egress_denied | 4 | A required egress row is not allowed. | Follow reason. |
template_not_published | 4 | The policy requires a promoted version and this file is not it. | ciresk dev templates publish <template>, then promote it. |
idle_out_of_bounds | 4 | --idle outside the organisation's range. | Pick a value within the bounds in the answer. |
session_too_large | 4 | The template reserves more memory or CPUs than the organisation allows one session. | Lower memory or cpus; ciresk dev policy show shows the cap. |
parameter_invalid | 2 | --param or --preset names nothing the template declares, or a value its type refuses. Each finding says what the parameter takes. | Fix the value. |
parameter_immutable | 4 | The running session has another value of an immutable parameter you named. | ciresk dev up --recreate with the same flags. |
parameters_changed | 4 | Only for an agent's dev_session_up: the running session has other values of the mutable parameters it named. ciresk dev up answers it by starting the session again. | Call again with recreate: true. |
pull_secret_not_granted | 2 | A pull.secret that your organisation's secrets do not grant to this session. | ciresk dev secrets set <name> --host <registry> --repo <repository> (secrets.md). |