Create, run, and manage boxes — the core sandbox lifecycle object in Zomg.
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.
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 --currentzomg target listzomg target use devzomg config get current
The selected target (--target, ZOMG_TARGET, --profile, ZOMG_PROFILE, or current in runtime target config).
Profile-scoped env vars such as ZOMG_PROFILE_<NAME>_URL.
ZOMG_URL.
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=stagingexport ZOMG_PROFILE_STAGING_URL=https://api.staging.example.comexport 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
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:
--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 list (alias ls) prints a table with NAME STATUS BASE PUBLISHED columns. The PUBLISHED column shows explicit https://<host> -> <port> mappings joined by commas.
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.
Flag
Purpose
--force, -f
Delete even if the box is running.
--wait
Poll 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, -j
Project / 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:
Status
Meaning
deleting
Cleanup is still in progress or retrying.
deleted
Cleanup finished, or no box by that name exists.
failed
Background retry exhausted and manual repair may be needed.
active
The box exists and is not currently being deleted.
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.
Flag
Purpose
--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, -f
Take over a name already published elsewhere.
--project, -p / --json, -j
Project / 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.
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 request
Resolves to
<box>
A box in the same project P.
<box>.<other-project>
A box in another project.
any dotted FQDN
External DNS.
# from inside a box, reach a sibling box named "db" on port 5432psql -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.
zomg exec <box> [flags] -- <cmd...> runs a command in a box.
Flag
Purpose
--user <NAME>
Run as a deployment user (default: yourself when signed in).
--root
Run as root (explicit escalation).
--env KEY=VALUE
Set an environment variable (repeatable).
--wd <DIR> (alias --cwd)
Working directory.
--sh (alias --bash)
Treat the args as a shell string.
--bg
Run detached in the background; returns a PID.
--project, -p / --json, -j
Project / 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.
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:
zomg ssh web-1zomg ssh web-1 --infozomg 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.
Flag
Purpose
--user <NAME>
Connect as a deployment user (default: yourself when signed in).
--root
Connect 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).
--info
Print 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.
zomg cp ./app.tar web-1:/srv/app.tarzomg get web-1:/var/log/app.log ./app.logzomg 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).
Deployment-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.
zomg user create deploy --shell /bin/bashzomg 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.
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 --versionzomg 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.
zomg inject <box> copies local credentials into the box.
Flag
Default
--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, -p
Target 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>
--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.