A box is a full Linux environment you can exec into. It boots from a base image and keeps its filesystem across stop and resume. Under the hood it’s a single-replica Kubernetes Deployment with a headless Service, backed by a copy-on-write btrfs root filesystem. You manage boxes through the zomg CLI. Every command on this page targets a box by its ref. Read reference resolution first if a command can’t find your box.

Projects and reference resolution

Every box lives in a project. A box’s identity is the pair (project, name), so the same name can exist in different projects.

Project resolution

The CLI resolves the project in this order:
  1. The --project / -p flag, or an explicit project:box ref.
  2. The ZOMG_PROJECT environment variable — set automatically inside every box.
  3. The git repository top-level directory name, if it’s a valid project name.
There is no fallback project. If none of these applies, the command errors:
Error: no project: pass --project <name>, set ZOMG_PROJECT, or run inside a git repository
A project named default is not special — it behaves like any other project. Run zomg project current to print the resolved project.
zomg project current

Box ref forms

A box ref accepts several forms:
FormMeaning
nameBox name in the resolved project.
project:nameCanonical explicit project (colon).
project.nameSame, dot sugar (colon is tried first, then dot).
.The current box, from ZOMG_PROJECT + BOX_NAME (errors if unset).
-An ephemeral box (see Ephemeral boxes).

Naming rules

  • Project: 1–63 characters, a-z, 0-9, -; starts and ends alphanumeric; no --.
  • Box name: a DNS label — 1–63 characters, a-z, 0-9, -; starts and ends alphanumeric.
  • Tag keys and values: may also use _ and ..

Target selection and profiles

The CLI needs a base URL and a token to reach the API. This runtime target config lives at $ZOMG_HOME/config.toml, or $XDG_CONFIG_HOME/zomg/config.toml when ZOMG_HOME is unset:
zomg target add dev --api-url https://api.192.168.86.2.sslip.io --ssh-host ssh.192.168.86.2.sslip.io --token-env ZOMG_TOKEN_DEV --insecure --current
zomg target list
zomg target use dev
zomg config get current
current = "dev"

[targets.dev]
api_url = "https://api.192.168.86.2.sslip.io"
insecure = true
auth = { token_env = "ZOMG_TOKEN_DEV" }

[targets."zomg.ai"]
api_url = "https://api.zomg.ai"
ssh_host = "ssh.zomg.ai"
auth = { token_env = "ZOMG_TOKEN_ZOMG_AI" }
The URL resolves in this priority:
  1. The --url flag.
  2. The selected target (--target, ZOMG_TARGET, --profile, ZOMG_PROFILE, or current in runtime target config).
  3. Profile-scoped env vars such as ZOMG_PROFILE_<NAME>_URL.
  4. ZOMG_URL.
  5. ZOMG_API_URL.
Most of the time you don’t pass any of these. Set current in runtime target config and every command targets it. Use the global --target <name> flag or ZOMG_TARGET=<name> to target a different runtime target for a single command. --profile <name> and ZOMG_PROFILE=<name> still select the same runtime target name for command-line convenience. For scripts that do not use config.toml, target/profile variables are named ZOMG_PROFILE_<UPPERCASED_NAME>_<SUFFIX>, where any non-alphanumeric character in the name becomes _. The suffixes include URL, TOKEN, SSH_HOST, INSECURE, and IP.
export ZOMG_TARGET=staging
export ZOMG_PROFILE_STAGING_URL=https://api.staging.example.com
export ZOMG_PROFILE_STAGING_TOKEN=...
zomg list
If a target is selected but no API URL is found, the CLI errors:
Error: target "staging" is selected but no api_url was found in target config or ZOMG_PROFILE_STAGING_URL

Create a box

zomg create web-1 --base box-base --cpu 2 --memory 2G
zomg create <name> provisions a new box. On success it prints OK, or raw JSON with --json.
FlagPurpose
--base, -b <BASE>Base image to create from. Defaults to box-base.
--fork, -f <BOX>Clone another box’s filesystem instead. Mutually exclusive with --base.
--tag, -t key=valueAttach a tag (repeatable). Used by list -t filtering.
--data, -d NAME=/mount/path[:ro|:rw]Attach a data volume (repeatable, default :rw).
--env, -e KEY=VALUESet a container environment variable (repeatable).
--cpu <CPU>CPU request, e.g. 2 or 500m.
--memory <MEM>Memory limit, e.g. 2G or 512M.
--project, -p <PROJECT>Target project.
--json, -jJSON output.

Base vs clone

Pass --base to start from a template image, or --fork to clone an existing box. You can’t use both:
Error: --base cannot be used with --fork
A --fork clone snapshots the source root and each non-system data volume into generated box-owned volumes named like box-<box-prefix>-<12 hex>. Those generated volumes are attached to the new box, hidden from normal attachment by ownership metadata, and deleted with the owning box after deletion fully completes. The clone inherits the source’s base, config, tags, env (merged with your overrides), and termination grace. Publishes are not cloned. You can’t clone a box from itself. Tagged base builds resolve through the alias:tag form:
zomg create db-1 --base box-base-postgres-17:0123abcd
See Snapshots & bases for building and tagging bases.

Data volumes

--data attaches a data volume at a mount path. The mount path must be absolute and cannot be / or sit under /proc, /sys, /dev, /run, /etc, /volumes, /home, or /box. Data volumes are deployment-global, so the volume need not be in the box’s project; each mount name must be unique.
zomg create web-1 --data assets=/srv/assets:ro --data uploads=/srv/uploads
See Data volumes for managing volumes, attachments, and backups.
Reserved environment keys are rejected at create time: ZOMG_PROJECT, BOX_NAME, ZOMG_API_URL, ZOMG_API_HOST, and TERM.

List, show, update

list

zomg list
zomg list --all --tag team=web
zomg list (alias ls) prints a table with NAME STATUS BASE PUBLISHED columns. The PUBLISHED column shows explicit https://<host> -> <port> mappings joined by commas.
FlagPurpose
--all, -aList across all projects (adds a PROJECT column).
--quiet, -qNames only; with --all, prints project:name.
--tag, -t key=valueFilter by tag (repeatable, AND).
--project, -p / --json, -jProject / JSON output.

show

zomg show web-1
zomg show <box> prints details: name, project, status, base, timestamps, publishes, tags, data mounts, and (from config) cpus, ram_mb, and storage_gb.

update — what it actually changes

zomg update web-1 CPU=4 MEMORY=4G
zomg update <box> [SETTINGS...] takes positional KEY=VALUE settings plus --tag, -p, and -j. Supported settings are:
  • CPU — updates the CPU resource.
  • MEMORY — updates the memory resource.
  • TERMINATION_GRACE_PERIOD_SECONDS — up to 86400.
  • Tags.
Every other KEY=VALUE is rejected. If no supported update is present, the server returns 400 no_updates ("no updates provided").
To change a box’s container environment after creation, recreate the box — optionally with --fork to keep the existing root filesystem.

Stop, resume, delete

stop and resume

zomg stop web-1
zomg resume web-1
zomg stop tears down the pod but preserves the filesystem. zomg resume recreates the pod and re-injects environment variables. Both print OK.
The pod IP changes on stop/resume because a box is single-replica. Clients reaching a box over inter-box DNS should reconnect after a resume.

delete

zomg delete web-1 web-2
zomg delete web-1 --force
zomg delete <box...> (alias rm) deletes one or more boxes. Per box it prints <name>: deleted or <name>: error: <msg>; any error exits non-zero.
FlagPurpose
--force, -fDelete even if the box is running.
--waitPoll until API cleanup fully completes instead of accepting an async delete.
--wait-timeout <SECONDS>Maximum wait time with --wait. Defaults to 240 seconds.
--project, -p / --json, -jProject / JSON output.
Without --force, deleting a running box returns 409 box_running ("box is running"). Delete is idempotent. By default, if Kubernetes or filesystem cleanup is accepted but not fully complete, the API returns 202 and the CLI prints <name>: deleting while a background retry continues. Pass --wait when the caller needs a terminal result before continuing. The CLI polls GET /v1/projects/{project}/boxes/{name}/delete-status until the status is deleted or failed, or until --wait-timeout expires. Delete status values are:
StatusMeaning
deletingCleanup is still in progress or retrying.
deletedCleanup finished, or no box by that name exists.
failedBackground retry exhausted and manual repair may be needed.
activeThe box exists and is not currently being deleted.

Publish a port

zomg publish web-1 --port 8080 --as app
zomg publish web-1 --port 3000
zomg publish web-1 --port 9229 --as debugger
zomg publish web-1
zomg unpublish web-1 app
Boxes publish nothing by default. zomg publish <box> --port <PORT> --as <NAME> creates one explicit public mapping from a box port to https://<NAME>.zomg.ai. Omit --as to let Zomg generate an adjective-noun host; collision retries add a short suffix. zomg publish <box> lists existing publishes. A box can have many publishes, each with its own host and port. unpublish removes one mapping.
FlagPurpose
--port <PORT>Box port to publish. Required when creating a publish; omitted to list publishes.
--as <NAME>Optional host name under the Zomg domain. Omitted for a generated name.
--force, -fTake over a name already published elsewhere.
--project, -p / --json, -jProject / JSON output.
Every publish record has the same shape: {"host":"app.example.com","url":"https://app.example.com","port":8080}. On success the CLI prints the public URL, or the record as JSON. zomg publish <box> --json (list form) prints {"publishes":[...]} of those records, and box JSON carries the same list under publishes. zomg unpublish <box> <name-or-host> --json prints {"host":"..."} for the removed mapping; unpublishing an already-absent host succeeds (idempotent). Publishing a host already in use returns 409 unless you pass --force. Unpublishing a host not owned by the box returns 403.

Inter-box DNS

Boxes reach each other by name. The Service is headless (clusterIP: None): it exposes all ports and resolves DNS straight to the pod IP, so traffic is not load-balanced. From inside a box in project P:
You requestResolves to
<box>A box in the same project P.
<box>.<other-project>A box in another project.
any dotted FQDNExternal DNS.
# from inside a box, reach a sibling box named "db" on port 5432
psql -h db -p 5432 -U postgres
Because a box is single-replica, its pod IP changes on stop/resume. Reconnect after the target box resumes.
More on the in-box runtime, PATH, and environment variables is on the Environment page.

Run commands with exec

zomg exec web-1 -- ls /srv
zomg exec web-1 --sh -- "cat /etc/os-release | head"
zomg exec web-1 --bg -- ./long-job.sh
zomg exec <box> [flags] -- <cmd...> runs a command in a box.
FlagPurpose
--user <NAME>Run as a deployment user (default: yourself when signed in).
--rootRun as root (explicit escalation).
--env KEY=VALUESet an environment variable (repeatable).
--wd <DIR> (alias --cwd)Working directory.
--sh (alias --bash)Treat the args as a shell string.
--bgRun detached in the background; returns a PID.
--project, -p / --json, -jProject / JSON output.
With neither --user nor --root, the command runs as the caller: a signed-in caller acts as their own deployment user, and the anonymous deployment token acts as root. See Environment → Users. Foreground (default) runs an interactive exec session and preserves the command’s exit code. If you give no command and stdin isn’t a TTY (and you didn’t pass --bg), it defaults to bash. --json is rejected in foreground mode:
Error: --json is not supported in foreground mode
Background (--bg) starts a detached process and prints pid=<n>. Use - as the box name to run in an ephemeral box.

Inspect with ps and logs

zomg ps web-1
zomg logs web-1 --pid 4123 --follow
zomg ps <box> runs ps inside the box (plus any extra args after --). zomg logs <box> --pid <PID> (alias tail) reads a background process’s log. --pid is required:
FlagPurpose
--pid <PID>Process to read (required).
--lines, -l <N>Number of lines (default 100).
--follow, -fTail in follow mode. Cannot combine with --json.
--project, -p / --json, -jProject / JSON output.

Open a shell with console

zomg console web-1
zomg console - --command "python3"
zomg console <box> opens an interactive shell. Use - for an ephemeral box that’s deleted on exit.
FlagPurpose
--tty / --no-ttyForce or disable an interactive TTY.
--raw / --no-rawForce or disable local terminal raw mode.
--term <TERM>Terminal type (defaults to TERM or xterm-256color).
--user <NAME>Run as a deployment user (default: yourself when signed in).
--rootRun as root (explicit escalation).
--command <CMD>Run this instead of a login shell.
--wd <DIR> (alias --cwd)Working directory.
--log-console-debugPrint transport details to stderr.
--project, -pTarget project.
console has no --json.

SSH via the gateway

zomg ssh web-1
zomg ssh web-1 --info
zomg ssh web-1 -- -A
zomg ssh <box> connects through the SSH gateway. When you are signed in it emits the username <project>--<box>--<user>@<host> so the session runs as your deployment user; --root emits <project>--<box>@<host> for a root session.
FlagPurpose
--user <NAME>Connect as a deployment user (default: yourself when signed in).
--rootConnect as root (explicit escalation).
--host <HOST> (env ZOMG_SSH_HOST)Gateway host. Otherwise resolved from the profile’s _SSH_HOST or _URL host.
--port <PORT> (env ZOMG_SSH_PORT)Gateway port (default 2222).
--infoPrint the resolved ssh command and exit.
-- <ssh-args>Extra arguments passed to ssh.
On Unix, zomg ssh execs into the real ssh client. Resolving the default identity calls GET /v1/me; --root or an explicit --user skips that lookup.

Ports

zomg port list web-1
zomg port wait web-1 8080 --timeout 30
zomg port <list|wait> (alias ports) inspects listening ports.
  • port list <box> (alias ls): --tcp, --udp (default both), -p, -j. Columns: PROTOCOL ADDRESS PORT.
  • port wait <box> <port>: --tcp (default) or --udp, --timeout <s> (default 60, 0 = forever), --interval <s> (default 1), -p, -j. Passing both --tcp and --udp, or --interval 0, errors.

Copy files

zomg cp ./app.tar web-1:/srv/app.tar
zomg get web-1:/var/log/app.log ./app.log
zomg put ./config.yaml web-1:/etc/app/config.yaml
zomg cp <src> <dest> transfers files. zomg get and zomg put are aliases. Exactly one side must be a box path written as box:/path (the :/ is required) — both-remote or neither-remote errors. Qualified refs work: project:box:/path, .:/path (current box), and -:/path (ephemeral). File permissions are preserved in both directions.
Directories aren’t supported yet:
Error: only files are supported (directories require -r, not yet supported)
Data-volume gotcha: zomg cp writes into the box root filesystem and the system volumes only. A path under a custom data-volume mount is not redirected — it lands in the root subvolume, where the live data mount shadows it at runtime. So zomg cp does not write into attached data volumes. To put files into a data volume, run zomg exec inside the box (for example, pipe through a shell command).

System volumes

Every box auto-mounts these volumes:
MountScope
/box/binGlobal, on PATH.
/box/allGlobal shared data.
/box/projPer-project; /box/proj/bin is on PATH.
/homeDeployment-global user homes, shared by every box in every project.
The in-box environment snapshot file is /etc/boxes-env. Injected variables include ZOMG_PROJECT, BOX_NAME, ZOMG_API_URL, ZOMG_API_HOST, plus node/host IPs and TERM. See Environment for the full runtime layout.

Users

zomg user create deploy --shell /bin/bash
zomg user list
zomg user <list|create|update|delete> manages the deployment’s single user registry — there is no --project. Created users get a synthesized UID/GID (stable, starting at 20000) and a home at /home/<user> on the deployment-global home volume, shared by every box in every project. /home/<user> survives box stop/resume and delete.
  • user create <name> [--shell <SHELL>] — prints name<TAB>uid<TAB>home.
  • user list — prints name uid home, tab-separated.
  • user update <name> [--shell <SHELL>] — updates the shell.
  • user delete <name> [--purge-home] — removes the user; keeps /home/<user> unless --purge-home is passed (no --json).
All except delete accept -j. Reference a user from exec, console, and ssh with --user — the user must already exist. You usually don’t need zomg user create for yourself: on first allowlisted Google sign-in (or first GET /v1/me), Zomg auto-provisions a user named after your email’s localpart (lowercased; characters outside [a-z0-9_-] become -) and binds the email to it. A pre-created user with that name and no email is claimed by the sign-in. Signed-in exec, console, and ssh then default to that user.

Ephemeral boxes

Pass - as the box name to exec, console, or cp/get/put to auto-create a box, run, and auto-delete it.
zomg exec - -- python3 --version
zomg console -
The generated name is ephemeral-<pid>-<millis>, and the CLI prints ephemeral box: <name> to stderr. Use project:- to target a specific project. --json isn’t supported for ephemeral exec, and ephemeral deletes always force.

Inject credentials

zomg inject web-1
zomg inject web-1 --ssh-key ~/.ssh/id_ed25519
zomg inject <box> copies local credentials into the box.
FlagDefault
--codex-auth <PATH>~/.codex/auth.json
--codex-toml <PATH>~/.codex/config.toml
--ssh-key <PATH>First of ~/.ssh/id_ed25519, id_rsa, id_ecdsa
--project, -pTarget project
Destinations inside the box: /root/.codex/auth.json (chmod 600), /root/.codex/codex.toml (renamed from config.toml), /root/.ssh/<keyname> (chmod 600) plus .pub (644), and /root/.gitconfig. Git config is auto-discovered from GIT_CONFIG_GLOBAL, $XDG_CONFIG_HOME/git/config, ~/.config/git/config, or ~/.gitconfig. It seeds /root/.ssh/known_hosts with GitHub’s keys and tests ssh -T git@github.com. The final line reads:
OK: injected codex auth/config, ssh keypair[, git config] into <project>:<box>
inject has no --json.

JSON output

--json / -j is supported on create, list, show, update, publish, unpublish, delete, stop, resume, background exec, non-follow logs, port list, port wait, and the user list/create/update commands. It is not supported on foreground exec, ephemeral exec, logs --follow, console, ssh, cp/get/put, inject, or user delete.

CLI reference

These docs are served per deployment as a docs pack at https://docs.zomg.ai. The full HTTP surface is documented in the API reference.

Next steps

Environment

The in-box runtime: PATH, /box/* volumes, env vars, and DNS.

Data volumes

Attach, share, and back up persistent data across boxes.

Services

Expose long-running services on a public URL with a port.

Snapshots & bases

Capture filesystem state and build tagged base images.