Config reference
The deploy config is a file named ox.toml at your repo root. It names the web process, workers, cron jobs, services, and build. Every key is optional: detection fills what you leave out from the repo's own files, and never overrides a declared value. Unknown keys, wrong types, and bad values are errors, and every problem is listed at once. Secrets never live in this file; they go in the dashboard's Variables tab.
domains = ["example.com"] # served by [app], or by [static] when there is no [app]
packages = ["ffmpeg"] # apt system libraries only (no toolchains)
[app] # the one web process; gets $PORT
start = "serve" # default: detected
health = "/healthz" # HTTP 2xx/3xx on this path; default: TCP connect
port = 9034 # pin; default: allocated and recorded
memory = "512M" # this process's cap
sandbox = "relaxed" # the only value; default is strict
[static] # files served by Caddy
dir = "dist" # relative to the repo root
spa = true # unknown paths serve index.html
api = ["/api", "/admin"] # these prefixes go to [app]; requires [app]
paths = { "/static" = "backend/static" } # more built dirs, each at its URL path
[build] # runs as the project user, variables available
commands = ["make build"] # default: detected
migrate = "make migrate" # after build, before the switch; default: detected
[workers]
worker = "celery -A app worker" # short form
bot = { run = "python bot.py", memory = "512M", sandbox = "relaxed", port = true, health = "/health" }
[cron] # 5-field cron or @hourly/@daily/@weekly/@monthly, UTC
digest = { schedule = "0 7 * * *", run = "python manage.py send_digest" }
[services] # postgres, redis, neo4j, qdrant only
postgres = { version = "18", extensions = ["vector"] }
redis = {}
qdrant = { only_for_this_project = true, port = 9200 }
[tools] # any mise tool name = version; default: from repo files
node = "24"
bun = "1.3"
[storage]
keep = ["media"] # names under the project data dir, survive releases
[limits] # the project's whole slice (processes + builds)
memory = "1G"
cpu = 1.5 # cores
[models]
huggingface = ["org/repo", "org/repo@revision"]
Validate locally
Run ox check [dir] (default .) on your machine before pushing. It is offline and reads nothing but the checkout. It prints the resolved plan with each value's source (declared, detected:<file>, default), the variables to set on the dashboard, and every problem at once. Fix them all until it prints Ready to deploy. ox check --json gives the same data as JSON.
These are the same checks the deploy runs. A bad manifest or a missing variable stops the deploy before anything on your server changes, with the exact reason on the run.
Top-level keys
domains: the names Caddy serves, served by[app], or by[static]when there is no app. Domains can also be added per project in the dashboard and apply on the next deploy.packages: Ubuntu system libraries (ffmpeg,libmagic1). Toolchains (nodejs,npm,python3-pip,golang*,rustc,cargo) are refused; declare them in[tools].- Git submodules are refused: releases are built from
git archiveof the commit.
[app]
The one web process, behind Caddy.
start: the command that serves HTTP on$PORT. Default: detected from the repo. Commands run withbash -euo pipefail -cin the release directory, with the tools onPATH;$PORTand other variables expand in the shell.health: a path that must return HTTP 2xx/3xx. Default: a TCP connect on the port.port: leave it out. ox allocates and records a port, and the zero-downtime switch runs two sides at once. A pin (1024-65535, unique on the host) runs one side and restarts in place, with a moment of downtime per deploy. Pin only for an app that hardcodes its port.memory: this process's cap (K/M/G). A value larger than[limits] memoryfails validation.sandbox: the only value is"relaxed"; the default is strict.
[static]
dir: the built files Caddy serves, relative to the repo root. It must exist after the build. Without[app], the project is a static site.spa: unknown paths serveindex.html.api: URL prefixes that stay proxied to[app]. Requires[app].paths: more built directories, each served at its URL path (a Djangocollectstaticoutput next to an SPA).dirmay be omitted when onlypathsis set.
[build]
commands: run as the project user, in order, in the new release. Default: detected from the lockfile and package scripts.migrate: runs after the build, before traffic switches. A snapshot of the database is taken first when it runs against a database with tables. Default: detected (Django, Prisma).
[workers]
Background processes, each its own systemd unit. A worker is a command string (short form) or a table: { run, memory, sandbox, port, health }.
- Names match
^[a-z][a-z0-9-]{0,27}$;appis reserved. port = trueallocates a port, exposed as that worker's$PORTand as a<NAME>_URLvariable (name upper-cased,-becomes_). This is how processes of one project call each other.healthneedsport = true: the health check calls the worker's$PORT.
[cron]
name = { schedule = "...", run = "..." }, rendered as a systemd timer.scheduleis a strict 5-field cron expression or@hourly/@daily/@weekly/@monthly, in UTC.
[services]
Four services ship. Declaring one is all you do: ox installs it, creates the database or instance, and writes the connection keys on every deploy.
postgres: shared, one database and role per project. Version 18. ProvidesDATABASE_URL. Extensions, likevector, come from the service:postgres = { extensions = ["vector"] }.redis: shared, one database index per project. Version 8. ProvidesREDIS_URL.neo4j: shared (private allowed). Version 5. ProvidesNEO4J_URI,NEO4J_USER,NEO4J_PASSWORD.qdrant: private only. Version 1. ProvidesQDRANT_URL.
only_for_this_project = trueruns a private instance with its own unit, its own data under the project's data dir, and its own port (allocated, or theportpin).porton a shared service fails validation.versionpins are enforced: a version ox cannot install on this OS fails with the available list.- Removing a service never drops data by itself. The service console shows it as unused, with its size and a Delete data button.
- To use your own external database, remove the service from
ox.tomland set the URL in Variables.
[tools], [storage], [limits], [models]
[tools]: any mise tool name = version (node = "24",uv = "0.11"). Default: read frommise.toml,.nvmrc, lockfiles,go.mod, and friends, or ox's default table. The first deploy records the resolved versions; a new ox release never silently changes them.[storage] keep: names under the project data dir (OX_DATA_DIR), writable and kept across releases, like["media"].[limits]:memorycaps the project's whole slice (processes and builds);cpuis a number of cores.[models] huggingface: model repos pulled into the project's cache on every deploy ("org/repo"or"org/repo@revision"). The environment gainsHF_HOME. Gated models need the Hugging Face token from Settings.
Detection
Detection runs on the checked-out commit and only fills keys the manifest leaves empty. It never overrides a declared value.
- Version files (
mise.toml,.tool-versions,.nvmrc,.python-version,package.jsonengines,go.mod,rust-toolchain.toml) fill[tools]; a detected language with no version file gets ox's default table. - Lockfiles fill the package manager, install, build, and start commands:
bun.lock,pnpm-lock.yaml,yarn.lock,package-lock.json,uv.lock,poetry.lock,requirements.txt. manage.pyfills Django migrate, collectstatic, and a gunicorn/uvicorn start.go.modfillsgo buildand the start binary.Cargo.tomlfillscargo build --release. A Vite build with no start script fills[static] distwithspa = true.- Only when the repo has no
ox.toml, dependencies also fill[services]: python or node postgres drivers imply postgres;redis,ioredis,bullmq,celery[redis]imply redis. With anox.toml,[services]is exactly what is declared; an implied but undeclared service shows as a hint on the review screen. - A repo with nothing to run and no detectable start command is asked for one on the review screen before the first deploy.
Variables
Three kinds, all managed in the dashboard's Variables tab. None of them live in ox.toml.
- Provided by ox, on every deploy:
PORT(per process),HOST,PUBLIC_URL,PUBLIC_HOST,<WORKER>_URLfor eachport = trueworker,OX_ENV,OX_PROJECT,OX_RELEASE,OX_DATA_DIR, the service keys above, andHF_HOMEwith[models]. Locked in the UI. Saving your own value under a provided key is refused; remove the service instead to use your own. - Yours: API keys and feature config, typed by you. Values 6 characters or longer are redacted in run logs. Saving variables deploys the current production commit, because build-time variables can change the build.
- Required: key names in the repo's
.env.example(or.env.sample/.env.template), at the root or one directory deep. The deploy is refused with the list until each one is set. A provided key or a key of yours satisfies one, even empty.
- A value may reference another:
${NAME}for any variable, or${service.field}with fieldhost,port,user,password,database, orurl, likeCELERY_BROKER_URL=${REDIS_URL}.$${is a literal${; a bare$NAMEis never expanded. Unknown references and cycles are refused at save.PORTandOX_RELEASEcannot be referenced. - ox never injects framework defaults (
DEBUG,SECRET_KEY,ALLOWED_HOSTS,CORS_*,NODE_ENV), never reads the repo's.env, and never edits a value of yours. Set framework variables as yours when the app needs them.
Examples
A built SPA with no backend (Caddy serves dist/, the repo's own build produces it):
domains = ["www.example.com"]
[static]
dir = "dist"
spa = true
Next.js SSR (start, build, and the node version come from package.json and the lockfile):
domains = ["app.example.com"]
[app]
health = "/"
Python with postgres, redis, a worker, cron, a migration, and persistent uploads:
[app]
start = "uv run python app.py"
health = "/health"
[build]
migrate = "uv run python migrate.py"
[workers]
worker = "uv run python worker.py"
[cron]
tick = { schedule = "* * * * *", run = "uv run python cron.py" }
[services]
postgres = {}
redis = {}
[storage]
keep = ["uploads"]
Go API with postgres and redis, a migration command, and a React SPA (Rust is the same shape: cargo build --release --locked and start ./target/release/server):
domains = ["api.example.com"]
[app]
start = "./server -port $PORT"
health = "/health"
[static]
dir = "dist"
spa = true
api = ["/api", "/health"]
[build]
commands = ["go build -o server ./cmd/server", "npm ci", "npm run build"]
migrate = "go run ./cmd/migrate"
[services]
postgres = {}
redis = {}
[tools]
node = "24"
A bot with no public URL: a shared neo4j, a private qdrant, and Hugging Face models. The worker binds its own port so its health check can call it:
[build]
commands = [
"uv venv --python 3.12 .venv",
"uv pip install --python .venv -r requirements.txt",
]
[workers]
bot = { run = "exec .venv/bin/python bot.py", port = true, health = "/health", memory = "1G" }
[services]
neo4j = {}
qdrant = {}
[models]
huggingface = ["KanariKanaru/nsfw-image-detection-384-onnx"]
[tools]
uv = "0.11"
[limits]
memory = "1500M"
Common mistakes
- Pinning
[app] portwhen the app reads$PORT. Leave it out; a pin costs a short restart per deploy. - Setting a provided key (
DATABASE_URL,REDIS_URL,PORT) as your own. Refused at save. Use an external database by removing the service and setting the URL as yours. - Git submodules. Refused; releases come from
git archive. Vendor the code. - Expecting framework env vars (
DEBUG,SECRET_KEY,ALLOWED_HOSTS,CORS_*,NODE_ENV). ox never injects them. - Toolchains in
packages. Refused; declare[tools]instead. - Legacy keys from the old engine (
runtime,package_manager,[deploy],[frontend],writable_paths, and the old array-of-table blocks). Each fails with the ox1 key that replaces it. {port}placeholders. Commands read$PORTfrom the shell.- A worker with
healthbut noport = true. Fails: the health check calls the worker's$PORT. - Cron in local time. Schedules run UTC.
static.apiwithout[app], ordomainswith neither[app]nor[static]. Both fail validation.- Absolute host paths under
/srv,/var,/etc,/opt, or/home. Refused; use paths relative to the repo root.
Agent instructions
The Copy skill for AI agent button at the top copies the bundled ox.toml skill below for your coding agent. It is the full authoring contract: schema, detection, services, variables, examples, and mistakes.
---
name: ox-manifest
description: Author and review ox deploy manifests (ox.toml at the repo root) for the ox1 engine and validate them with `ox check` before pushing. Use when generating or editing ox.toml, fixing a validation error, or preparing a repo for a deploy from the ox dashboard.
---
# ox manifest authoring
`ox.toml` sits at the repo root and is the whole deploy contract for the ox1 engine: the web process, workers, cron, services, build. No LLM runs at deploy time. ox deploys one repo onto one Ubuntu 26.04 host with systemd, Caddy, shared PostgreSQL 18 and Redis, and mise toolchains. There is no Docker. Releases are built from `git archive` of the commit, so git submodules are refused.
Use this skill whenever you generate or edit an `ox.toml`, fix a validation error, or prepare a repo for a deploy from the ox dashboard.
## Workflow
1. **Inspect the repo first.** Read the files that drive detection: lockfiles, `package.json`, `pyproject.toml` / `requirements.txt` / `manage.py`, `go.mod`, `Cargo.toml`, `mise.toml` / `.nvmrc` / `.python-version`, `prisma/schema.prisma`. The detection table below says what ox fills from each.
2. **Write the smallest ox.toml.** Declare only what detection cannot know: `domains`, the `[services]` the app needs, `[workers]`, `[cron]`, and any command detection gets wrong. Leave `[tools]` versions, `[build] commands`, `[app] start`, and `[static]` out when the repo names them. Detection never overrides a declared value. A repo with no `ox.toml` at all deploys zero-config, and dependencies even imply `[services]`; once an `ox.toml` exists, `[services]` is exactly what it declares.
3. **Run `ox check`** from the repo root. It is offline and reads nothing but the checkout. It prints the resolved plan with each value's source (`declared`, `detected:<file>`, `default`) and every problem at once. Fix every problem and rerun until it prints `Ready to deploy.` `ox check --json` gives the same data as JSON. Build the binary from the ox repo with `go build -o ox ./cmd/ox` when it is not installed. These are the same checks preflight runs: a bad manifest or a missing variable stops the deploy before the host is touched.
4. **List the variables the operator must set.** Read `.env.example` (or `.env.sample` / `.env.template`) at the root or one directory deep. Every key there that ox does not provide must be set in the dashboard's Variables tab, and the deploy is refused until each one is set. Hand the operator the list with a one-line note per key. Commit the example file with placeholder values only; real secrets never go in the repo.
5. **Push.** The plane polls the repo every minute and deploys new commits on the default branch (or the operator presses Deploy). A failed commit is not retried until the next push or a manual Deploy.
## Schema (TOML; every key is optional, unknown keys fail)
```toml
domains = ["example.com"] # served by [app], or by [static] when there is no [app]
packages = ["ffmpeg"] # apt system libraries only (no toolchains)
[app] # the one web process; gets $PORT
start = "serve" # default: detected (§4.2)
health = "/healthz" # HTTP 2xx/3xx on this path; default: TCP connect
port = 9034 # pin; default: allocated and recorded
memory = "512M" # this process's cap
sandbox = "relaxed" # the only value; default is strict
[static] # files served by Caddy
dir = "dist" # relative to the repo root
spa = true # unknown paths serve index.html
api = ["/api", "/admin"] # these prefixes go to [app]; requires [app]
paths = { "/static" = "backend/static" } # more built directories, each served at its URL path
[build] # runs as the project user, variables available
commands = ["make build"] # default: detected
migrate = "make migrate" # runs after build, before the switch; default: detected
[workers]
worker = "celery -A app worker" # short form
bot = { run = "python bot.py", memory = "512M", sandbox = "relaxed", port = true, health = "/health" }
[cron] # 5-field cron or @hourly/@daily/@weekly/@monthly, UTC
digest = { schedule = "0 7 * * *", run = "python manage.py send_digest" }
[services] # postgres, redis, neo4j, qdrant only
postgres = { version = "18", extensions = ["vector"] }
redis = {}
qdrant = { only_for_this_project = true, port = 9200 }
[tools] # any mise tool name = version; default: from repo files (§4.2)
node = "24"
bun = "1.3"
[storage]
keep = ["media"] # names under the project data dir, writable, survive releases
[limits] # the project's whole slice (processes + builds)
memory = "1G"
cpu = 1.5 # cores
[models]
huggingface = ["org/repo", "org/repo@revision"]
```
Rules behind the fields:
- **Names.** Worker and cron names match `^[a-z][a-z0-9-]{0,27}$`, and `app` is reserved.
- **Sizes and CPU.** `memory` takes `K`/`M`/`G` suffixes, `cpu` a positive number of cores. A per-process `memory` larger than `[limits] memory` fails validation.
- **Ports.** Leave `[app] port` out: ox allocates and records one, and both sides of the zero-downtime switch need their own. A pin (1024-65535, unique on the host) runs one side only and restarts in place, with a moment of downtime per deploy. `port = true` on a worker allocates a port and exposes it as that worker's `$PORT`; a worker with `health` must have `port = true`.
- **Commands** run with `bash -euo pipefail -c` in the release directory, with the tools on `PATH`. `$PORT` and other variables expand in the shell. There are no `{port}` placeholders.
- **`packages`** are Ubuntu system libraries (`ffmpeg`, `libmagic1`). Toolchains (`nodejs`, `npm`, `python3-pip`, `golang*`, `rustc`, `cargo`) are refused with the `[tools]` line that replaces them.
- **No host paths.** No value may name an absolute path under `/srv`, `/var`, `/etc`, `/opt`, or `/home`. `keep` and `dir` entries are relative and may not contain `..`.
- **`[static]`.** Without `[app]`, the project is a static site. `api` requires `[app]`. `dir` must exist after the build. `[static] dir` may be omitted when only `paths` is set.
## Detection (defaults only)
Detection runs on the checked-out commit, only fills keys the manifest leaves empty, and never overrides a declared value.
| Signal | Default filled |
|---|---|
| `mise.toml`, `.tool-versions`, `.nvmrc`, `.node-version`, `.python-version`, `package.json` `engines.node`/`packageManager`, `go.mod` `go`/`toolchain`, `rust-toolchain.toml` | `[tools]` versions |
| none of the above for a detected language | the ox release's default version table (`ox check` prints it) |
| `bun.lock`/`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json` | package manager, install command (frozen lockfile), `build` script, `start` script |
| `uv.lock`; `poetry.lock`; `requirements.txt` | `uv sync --frozen --no-dev`; `poetry install --only main`; `uv venv` + `uv pip install -r requirements.txt` |
| `manage.py` | Django: `migrate --noinput`, `collectstatic --noinput`; start with gunicorn/uvicorn when it is a dependency |
| `go.mod` | `go build -o .ox/bin/app .`, start `.ox/bin/app` |
| `Cargo.toml` | `cargo build --release`, start the package's binary |
| a Vite/SPA build with no start script | `[static] dir = "dist"`, `spa = true` |
| `index.html` at the root and nothing else | `[static] dir = "."` |
| `prisma/schema.prisma` | migrate `prisma migrate deploy` |
- Only when the repo has **no** `ox.toml`, detection also fills `[services]` from dependencies: `psycopg`/`psycopg2`/`asyncpg`/`pg`/`postgres`/Prisma with the `postgresql` provider imply postgres; `redis`/`ioredis`/`bullmq`/`celery[redis]` imply redis. With an `ox.toml`, `[services]` is exactly what is declared, and an implied but undeclared service shows as a hint on the review screen.
- The first successful plan records every resolved value with its source. Later plans reuse recorded values until the repo declares the key or the detection signal changes (a new `.nvmrc` re-resolves node; a new ox release never silently changes a recorded version).
## Services
Four entries ship. Declare each one you need; ox installs it, creates the database or instance, and writes the connection keys.
| Service | Mode | Version | Keys ox provides |
|---|---|---|---|
| `postgres` | shared: one database and role per project | 18 | `DATABASE_URL` |
| `redis` | shared: one database index per project | 8 | `REDIS_URL` |
| `neo4j` | shared (private allowed) | 5 | `NEO4J_URI`, `NEO4J_USER`, `NEO4J_PASSWORD` |
| `qdrant` | private only | 1 | `QDRANT_URL` |
- `only_for_this_project = true` runs a private instance with its own unit, its own data under the project's data dir, and its own port (allocated, or the `port` pin). `port` on a shared service fails validation.
- `version` pins are enforced: a version ox cannot install on this OS fails preflight with the available list.
- postgres extensions come from the service: `postgres = { extensions = ["vector"] }`.
- Removing a service from `ox.toml` (or deleting the project) never drops data by itself. The service console shows it as unused, with its size and a Delete data button.
## Variables
Three kinds, all managed in the dashboard's Variables tab:
- **Provided by ox**, on every deploy: `PORT` (per process), `HOST`, `PUBLIC_URL`, `PUBLIC_HOST`, `<WORKER>_URL` for each `port = true` worker (`http://127.0.0.1:<port>`, name upper-cased, `-` becomes `_`), `OX_ENV` (`production`/`staging`), `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, the service keys above, and `HF_HOME` with `[models]`. Locked in the UI; copy them out when a command needs one.
- **Yours**: API keys and feature config, typed by the operator. Values 6 characters or longer are redacted in run logs.
- **Required**: key names in the repo's `.env.example` / `.env.sample` / `.env.template`, at the root or one directory deep. A required key is satisfied by a provided key or a key of yours, even an empty one. Until then the deploy is refused with the list.
References: a value may contain `${NAME}` (any provided key or key of yours) or `${service.field}` with field `host|port|user|password|database|url`, for example `CELERY_BROKER_URL=${REDIS_URL}`. `$${` is a literal `${`. A bare `$NAME` is never expanded. An unknown reference or a cycle is refused at save. `PORT` and `OX_RELEASE` cannot be referenced. Saving variables deploys the current production commit, because build-time variables such as `VITE_*` can change the build.
ox never injects framework defaults (`DEBUG`, `SECRET_KEY`, `ALLOWED_HOSTS`, `CORS_*`, `FRONTEND_URL`, `NODE_ENV`), never reads the repo's `.env`, and never edits a value of yours. Set framework variables as yours when the app needs them.
## Worked examples
A built SPA with no backend (Caddy serves `dist/`, the repo's own build produces it):
```toml
domains = ["www.example.com"]
[static]
dir = "dist"
spa = true
```
Next.js SSR (start, build, and the node version come from `package.json` and the lockfile):
```toml
domains = ["app.example.com"]
[app]
health = "/"
```
Python with postgres, redis, a worker, cron, a migration, and persistent uploads:
```toml
[app]
start = "uv run python app.py"
health = "/health"
[build]
migrate = "uv run python migrate.py"
[workers]
worker = "uv run python worker.py"
[cron]
tick = { schedule = "* * * * *", run = "uv run python cron.py" }
[services]
postgres = {}
redis = {}
[storage]
keep = ["uploads"]
```
Go API with postgres and redis, a migration command, and a React SPA (Rust is the same shape: `cargo build --release --locked` and start `./target/release/server`):
```toml
domains = ["api.example.com"]
[app]
start = "./server -port $PORT"
health = "/health"
[static]
dir = "dist"
spa = true
api = ["/api", "/health"]
[build]
commands = ["go build -o server ./cmd/server", "npm ci", "npm run build"]
migrate = "go run ./cmd/migrate"
[services]
postgres = {}
redis = {}
[tools]
node = "24"
```
A bot with no public URL: a shared neo4j, a private qdrant, and Hugging Face models. The worker binds its own port so its health check can call it:
```toml
[build]
commands = [
"uv venv --python 3.12 .venv",
"uv pip install --python .venv -r requirements.txt",
]
[workers]
bot = { run = "exec .venv/bin/python bot.py", port = true, health = "/health", memory = "1G" }
[services]
neo4j = {}
qdrant = {}
[models]
huggingface = ["KanariKanaru/nsfw-image-detection-384-onnx", "navodPeiris/minilm-toxic-spam-classifier"]
[tools]
uv = "0.11"
[limits]
memory = "1500M"
```
## Common mistakes
- **Pinning `[app] port`** when the app reads `$PORT`. Leave it out: ox allocates and records one, and zero downtime needs both sides. Pin only for an app that hardcodes its port, and accept the short restart each deploy.
- **Setting a provided key yourself** (`DATABASE_URL`, `REDIS_URL`, `PORT`, ...) as a variable or in `.env.example` values. Refused at save. To use your own external database, remove the service from `ox.toml` and set the URL as yours.
- **Git submodules.** Releases come from `git archive`; submodules are refused. Vendor the code or drop the submodule.
- **Expecting framework env vars** (`DEBUG`, `SECRET_KEY`, `ALLOWED_HOSTS`, `CORS_*`, `NODE_ENV`). ox never injects them; set them as yours.
- **Toolchains in `packages`** (`nodejs`, `npm`, `python3-pip`, `golang`, `rustc`, `cargo`). Refused; declare `[tools]` instead.
- **Legacy keys.** Keys of the old engine (`runtime`, `package_manager`, `[deploy]`, `[frontend]`, `writable_paths`, and the old array-of-table blocks for processes, domains, services, models, cron jobs, and apt sources) all fail, each with the ox1 key that replaces it. ox1 spells services `[services] postgres = {}`, domains `domains = ["example.com"]`, and cron `[cron] name = { schedule = "...", run = "..." }`.
- **`{port}` placeholders** from the old engine. Commands read `$PORT` from the shell.
- **A worker with `health` but no `port = true`.** Fails: the health check calls the worker's `$PORT`.
- **Cron in local time.** Schedules run UTC.
- **`static.api` without `[app]`, or `domains` with neither `[app]` nor `[static]`.** Both fail validation.
- **Absolute host paths** under `/srv`, `/var`, `/etc`, `/opt`, or `/home`. Refused; use names relative to the repo root.
## Install this skill
From the ox repo root, copy the directory into the agent's skills folder:
```bash
# Grok Build (all projects)
mkdir -p ~/.grok/skills && cp -r skills/ox-manifest ~/.grok/skills/
# Claude Code (all projects)
mkdir -p ~/.claude/skills && cp -r skills/ox-manifest ~/.claude/skills/
```
For a single project, `.grok/skills/` or `.claude/skills/` under the repo root works too; commit it so every agent working in that repo loads it.