---
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, PostgreSQL 18, Redis, neo4j, and qdrant from apt or mise, 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] install`, `[build] commands`, `[app] start`, and `[static]` out when the repo names them. Detection never overrides a declared value. A build runs `[build] install` (the lockfile's install, detected) and then `[build] commands`; declaring `commands` replaces only the detected build steps, never the install. Declare `install` only when the packages are not installed from the root lockfile (a monorepo, a venv on a specific Python). 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. Read the `build.install` and `build.commands` rows: they are exactly what runs, in order. An app with dependencies and no `build.install` row ships a release with no packages, and it crashes at start. 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.** A push to the default branch deploys at once (GitHub's webhook), with a poll every minute as the fallback (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)
internal = true                   # serve nothing public: no preview host, no domain, no PUBLIC_URL; the app runs for its workers on loopback. Refused with `domains`

[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
install  = "npm ci"               # runs first; default: the lockfile's install
commands = ["make build"]         # runs after install; 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]                        # built-in types: postgres, redis, neo4j, qdrant, mysql
postgres  = { version = "18", extensions = ["vector"] }
analytics = { type = "postgres" } # a second database: ANALYTICS_DATABASE_URL
redis     = {}                    # private by default
qdrant    = { port = 9200 }       # private only; pin its port

[services.search]                 # a custom service: an entry with run
run      = "export MEILI_MASTER_KEY=\"$SERVICE_PASSWORD\"; exec meilisearch --db-path \"$SERVICE_DATA_DIR/db\" --http-addr 127.0.0.1:$PORT --env production"
tool     = "github:meilisearch/meilisearch@1.12.0"   # a mise tool; or packages = [...] (apt)
health   = "/health"              # HTTP 2xx/3xx; default: TCP connect
memory   = "512M"                 # cap and capacity floor; default 128M
provides = { MEILI_URL = "http://${host}:${port}", MEILI_MASTER_KEY = "${password}" }
backup   = true                   # default: its data dir is backed up daily

[tools]                           # a mise tool = version (no npm:/cargo:/pipx:-style package backends); default: from repo files (§4.2)
node = "24"
bun  = "1.3"

[storage]
keep = ["media"]                  # repo paths, linked to 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, `[build] install` (frozen lockfile), `build` script, `start` script |
| `uv.lock`; `poetry.lock`; `requirements.txt` | `[build] install`: `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

Each `[services]` entry is `<name> = { ... }` (names like workers'). Its type defaults to its name: `postgres = {}` is a postgres, `analytics = { type = "postgres" }` a second postgres for the same project. An entry with `run` is a custom service. ox installs each one, creates its database or instance, generates its password, and writes the connection keys.

| Type | Default mode | Version | Keys ox provides | Backup |
|---|---|---|---|---|
| `postgres` | shared: a database and role per entry | 18 | `DATABASE_URL` | `pg_dump` |
| `redis` | private (shared: an index per entry, no password) | 8 | `REDIS_URL` (private: carries its password) | private: files |
| `neo4j` | private (shared: one instance, one admin for all) | 5 | `NEO4J_URI`, `NEO4J_USER`, `NEO4J_PASSWORD` | private: files |
| `qdrant` | private only | 1 | `QDRANT_URL` | files |
| `mysql` | private only (MariaDB, MySQL-compatible) | 11 | `MYSQL_URL` | files |
| custom (`run`) | private only | its `tool` | its `provides`, or `<NAME>_URL` | files unless `backup = false` |

- `only_for_this_project = true` runs a private instance: the unit `ox-<project>-svc-<entry>.service` in the project's slice, as the project user, in the strict sandbox, with data under `$OX_DATA_DIR/services/<entry>/` and its own port (allocated, or the `port` pin). `only_for_this_project = false` uses the shared instance; `port` on a shared entry fails validation. The default mode is recorded, so a project whose redis resolved shared before keeps shared.
- An entry named like its type provides its keys as they are; any other entry prefixes them with its name upper-cased (`job-queue = { type = "redis" }` provides `JOB_QUEUE_REDIS_URL`). Two entries providing the same key fail validation.
- A custom service's `run` runs with `bash -euo pipefail -c` in its data dir with `PORT`, `SERVICE_DATA_DIR`, `SERVICE_PASSWORD`, and the tools' `PATH`, and none of the app's variables. `tool` is `name@version` (a mise tool); `packages` are apt names; `provides` values are templates over `${host}`, `${port}`, `${password}`, with upper snake case keys. `type`, `version`, `extensions`, and `only_for_this_project = false` are refused on it. Keep secrets off its command line: read `$SERVICE_PASSWORD` into the service's own env var. A Meilisearch, Typesense, Elasticsearch or OpenSearch, Redis, Valkey, ClickHouse, Chroma, or Memcached gets an Explore page (bind it to 127.0.0.1 on `$PORT`), known by its `tool`, `packages`, or `run`; set `explore = "<kind>"` only when none of those names it.
- Private instances start, and pass health, before migrate and the app, and restart only when their definition changes.
- `version` pins are enforced: a version ox cannot install on this OS fails preflight with the available list. postgres extensions: `postgres = { extensions = ["vector"] }`.
- Preflight checks the whole host has room: each shared instance once, every private instance of every project, every `[limits] memory`.
- Removing an entry from `ox.toml` stops it and keeps its data; the Services tab lists it as unused with its size and a Delete data button. Deleting the project keeps a 7-day trash with a backup of every entry that has a backup method.

## 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 `${entry.field}` (any `[services]` entry) 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 run build"]  # npm ci is detected from package-lock.json and runs first
migrate  = "go run ./cmd/migrate"

[services]
postgres = {}
redis    = {}

[tools]
node = "24"
```

A bot with no public URL: a private neo4j, a private qdrant, and Hugging Face models. The worker binds its own port so its health check can call it:

```toml
[build]
install = "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`.
- **Writing files next to the code at run time.** The release directory is read-only to the app (strict sandbox): a SQLite file, uploads, or logs at a relative path like `./data` fail with `EROFS`. List the folder in `[storage] keep` (a repo path like `backend/logs`): ox links it to a writable folder under `$OX_DATA_DIR` that survives releases, so the app keeps its relative path. A file at the repo root needs a folder of its own.
- **A `keep` path that is not where the app writes.** `keep` is the path from the repo root to the folder the app writes, not a label: a Django app writing `backend/logs/app.log` needs `keep = ["backend/logs"]`, and `keep = ["logs"]` makes a folder the app never sees. Check every relative path the app writes at run time (`MEDIA_ROOT`, log files, SQLite, upload dirs) against the list.
- **Repeating the root lockfile's install in `[build] commands`.** The detected install already runs first; repeating it installs twice. Leave it to detection, or move it to `[build] install` when it is not the root lockfile's.
- **`bun --cwd web run build`.** bun reads `--cwd` before `run` as its own flag, prints its help, and exits 0, so the build "passes" and builds nothing. Write `bun run --cwd web build`.
- **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.

## Deploying with the ox CLI (for AI agents)

The `ox` binary is also the client of ox Cloud (`https://deploywithox.com`, compiled in: no flag points it elsewhere). Every command takes `--json` (stable snake_case fields) and exits non-zero on failure, so read JSON and check the exit code: 1 a failed run or a refused request, 2 a usage error, 3 not signed in (run `ox login`), 4 the plane could not be reached. With `--json` a failure also writes `{"error": "...", "code": "<kind>"}` to stderr.

The CLI does everything the console does, so an agent never needs the browser except for `ox login` and Connect GitHub (a GitHub page: `ox repos` prints its link when nothing is connected). A project is `name` or `server/name` (or `--server`). `ox <command> -h` says more.

- `ox login` prints a code and a URL on stderr; a person approves it in the browser. The token lands in `~/.config/ox/credentials` (0600) and lasts 90 days. Never ask for or print the token.
- **Create, review, first deploy.**
  - `ox servers --json` lists servers; `ox servers add` prints the command a person runs on a fresh Ubuntu LTS VPS with sudo (it works once).
  - `ox repos --json` lists the repositories ox can read. `ox new owner/repo [--server S] --json` makes the project (a public https or an ssh URL works too) and prints its `name`.
  - `ox review <project> --json` says `ready`, or what is missing: `ask_start` (answer with `--start 'CMD'`, a command that serves HTTP on `$PORT`), `missing` variables (each with an `example` and `can_generate`), and `issues` to fix in `ox.toml`. An ssh repository shows a `deploy_key` a person adds to GitHub, read only.
  - Answer with `ox review <project> --start 'CMD' --generate SECRET_KEY --skip OPTIONAL_KEY --from-file .env.values --wait`: `--generate K` makes a random value, `--skip K` marks a key not needed, `--from-file F` (or `-` for stdin) carries the other values. It saves them and runs the first deploy. Then `ox review` again until `ready` is true, or deploy.
- **Deploy and watch.**
  - `ox deploy <project> --wait --json`: the run's log streams to stderr, `{"run": {...}}` goes to stdout, and the exit code is the run's result. On a failure, read `run.detail`, then `ox logs <project> --run <id>`.
  - `ox status <project> --json`: live commit and release, URL, processes, last run. `ox runs <project>` lists runs. `ox promote <project> --yes --wait` deploys production at the commit staging runs (printing the commits first): production keeps its own variables, data, and domains, and a key the new code needs must be set on production before. `ox crons <project> [--json]` lists the `[cron]` jobs with their next run and newest runs (start, end, exit code); `ox crons <project> run NAME` runs one now. `ox celery <project> <worker> [--json]` reads a Celery worker's app: workers online, running, held, and scheduled tasks, queue depths. `ox logs <project> [-n N] [--follow]` prints the processes' logs, redacted. To look back, `ox logs <project> --since 2h [--until T] [--grep TEXT | --regex RE] [--process P] [--severity warn] [-n N]` searches the server's journal, and `-o FILE` saves everything a range and search match (up to 100 MB). `ox metrics <project> [--range 1h|24h|7d|30d] [--json]` is the usage history: CPU and memory as min, average, and max, with the deploys and alerts in the range (what happened overnight).
  - `ox rollback <project> [--release ID] [--restore-variables] --wait` goes back to a kept release; `--restore-variables` also brings back the variables it ran with. `ox stop <project>` and `ox start <project>` switch it off and on.
- **Variables and services.** A variable is a key, a value, and a source: `yours`, `provided` (ox's, locked), `inherited` (a copy's value still equal to production's), or `needed` (the repo's `.env.example` asks and nothing sets it). `ox vars <project> --json` lists them without values; `--reveal` shows values and is written to the audit log, so use it only when a person asks.
  - Change them with the verb first: `ox vars set <project> KEY [KEY...]` reads each value from standard input (one key: the value itself; several: `KEY=VALUE` lines), `ox vars set <project> --from-file F` (or `-`) reads a `.env` file, `ox vars unset <project> KEY...`, `ox vars import <project> F --yes` merges a `.env` file. Never put `KEY=VALUE` on the command line: it is refused, because it would keep the secret in the shell history. Saving deploys; add `--no-deploy` to save only (the table says "not live" until the next deploy). A provided key is refused.
  - `ox vars pull <project> -o F` writes your values to a new 0600 file (never over one without `--force`) and is audited like `--reveal`. `ox vars history <project>` lists saved versions, and `ox vars restore <project> VERSION --yes` brings one back. A staging copy or preview is a project of its own (`ox vars <project>-staging`); `ox vars sync <copy> [KEY...] --yes` takes production's values.
  - When `ox deploy` or a push's deploy is refused for missing variables, `ox vars <project> --json` lists them in `needed`; set them, then deploy.
  - `ox services <project> --json` gives each entry's type, mode, version, state, provided key names, and backups.
- **Domains and copies.** `ox domains <project>` lists domains with their DNS state (`waiting` means the A record is not pointing at the server yet); `ox domains <project> add shop.example.com --wait` and `remove`. `ox staging <project> --wait` makes the staging copy (`ox delete <project>-staging --confirm <project>-staging` removes it). `ox previews|autodeploy <project> [on|off]` show or set branch previews and deploy on every push. `ox notifications [--project P] [on|off EVENT...]` chooses which events (`deploy_failed`, `site_down`, `cpu_high`, ...) email the owner and reach the webhook. `ox rename <project> <new>` works only before the first deploy.
- **Backups and data.** `ox backups <project> --json` lists each entry's backups (`source` is what a restore takes); `ox backups <project> now <entry> --wait`; `ox backups <project> download <entry> <source> --output F`. `ox restore <project> <entry> --from <source> --confirm <project> --wait`, or `--file dump.sql` to upload one (up to 95 MB). `ox data <project>` lists data of removed services and `ox data <project> drop <entry> --confirm <entry>` deletes it.
- **Look into a service.** `ox explore <project> <entry> --json` gives the overview of a postgres, redis, neo4j, or qdrant entry; `tables` or `keys [PATTERN]` list what it holds, `table NAME` or `key KEY` open one item, `activity` shows sessions or slow commands. `ox explore <project> <entry> query` reads one read-only query from stdin or `--from-file` (SQL, a redis read command, Cypher, or for qdrant `search|scroll|count COLLECTION` and a JSON body). Results can be partial: `truncated` and `next` (continue with `--cursor`) say so. Changes (`allow-changes`, then `change cancel|end-session|delete-key|set-ttl ... --confirm <entry>`) alter data: only when a person asked.
- **Account.** `ox tokens` lists and `ox tokens create --name N` prints a new token once (keep it secret); `ox settings --json` shows what is set, never a secret; `ox settings s3 ... --from-file F`, `ox settings huggingface set --from-file F`, and `ox settings preview-domain D` change it. `ox servers forget <server> --confirm <server>` and `ox servers update <server>` need a server with no projects and a connected agent. `ox account export [-o FILE]` downloads everything the plane holds about the account (no secret in it); `ox account delete --confirm <github-login>` deletes the account for good and is refused while any server is left, so run it only when the owner asked.
- **Destructive commands need the name typed again.** `delete`, `restore`, `servers forget`, `webhooks remove`, `data drop`, `explore ... change`, and `account delete` refuse (exit 2) without `--confirm <name>`: the project's name, the server's, the endpoint's id, the entry's, or your GitHub login. Pass it only when a person asked for that action on that name; never to get past the refusal on your own.
- **Webhooks.** `ox webhooks` lists endpoints; `echo URL | ox webhooks add - --events deploy.failed,site.*` adds one (the signing secret is printed once); `test`, `deliveries`, `redeliver`, `rotate`, `enable`, `disable`, `remove ID --confirm ID`, `event-types`. `ox notifications` is email only.
- **Secrets never go in a flag.** The S3 secret key, the Hugging Face token, and a webhook endpoint's address and header value are read from `--from-file F` or stdin (`-`); a flag with the value is refused. Do not echo them, and do not paste a printed token or webhook secret anywhere else.

A typical loop: edit `ox.toml`, run `ox check`, push (a push deploys on its own), or run `ox deploy <project> --wait --json`, and fix what the review or the run log says.

## Install this skill

Download it from ox Cloud into the agent's skills folder:

```bash
# Grok Build (all projects)
mkdir -p ~/.grok/skills/ox-manifest && curl -fsSL https://deploywithox.com/ox-skill.md -o ~/.grok/skills/ox-manifest/SKILL.md
# Claude Code (all projects)
mkdir -p ~/.claude/skills/ox-manifest && curl -fsSL https://deploywithox.com/ox-skill.md -o ~/.claude/skills/ox-manifest/SKILL.md
```

For a single project, `.grok/skills/ox-manifest/` or `.claude/skills/ox-manifest/` under the repo root works too; commit it so every agent working in that repo loads it.
