Command Reference
The corral command-line interface provides full control over your local development environment. Every feature available in the macOS app is also available from the terminal.
All CLI commands communicate with a background daemon that manages process state. The daemon starts automatically on the first command — you don’t need to start it manually.
Commands marked with PRO require an active Corral Pro license. Learn more → — or activate a license from the in-app Subscription room.
Global Flags
| Flag | Description |
|---|---|
--json | Output results as JSON instead of formatted text |
--data-dir <path> | Override the data directory (default: ~/.corral) |
The data directory can also be set via the CORRAL_DATA_DIR environment variable.
Addressing things by name
Every command that names a project, service, stack or unit takes the same kind of argument — a selector:
| Form | Example | Notes |
|---|---|---|
| Name | storefront | Exact match wins; otherwise a prefix completes it |
kind:name | project:compliancr | When a name belongs to more than one kind |
| Subdomain | api-2 | Projects only, and unique by construction |
Service name@version | postgres@16.4 | A bare postgres works when one version is installed, or a default is set |
| Infra program name | caddy | caddy, dnsmasq and helper reach the Reverse proxy, DNS resolver and System helper |
| UUID | 885624ef-f68a-… | |
| Id prefix | 885624ef | At least 8 characters, and unambiguous |
The kind words are exactly what the KIND column of corral status prints — project, service, stack, infra, and so on. There is nothing extra to learn.
An alias is a handle, not a rename. caddy resolves to the Reverse proxy unit; that unit is still displayed as Reverse proxy everywhere, including the name field of corral --json status. So corral status caddy works, and a script filtering on .name == "caddy" still matches nothing — filter on the real name, or select by id.
A prefix completes a name; an infix does not. store finds storefront. It does not find my-store-api — matching the middle of a name is surprising, and surprising is dangerous on a command that deletes something.
Destructive commands need the whole name. corral project remove, corral stack delete, corral service uninstall and corral data secrets delete won’t complete a partial name, and they confirm before acting. Pass --yes (or --force) to skip the prompt in a script; the exact-name rule still applies, because a script never sees the prompt.
Nothing is ever picked for you. If a selector matches more than one thing, Corral lists the matches with what tells them apart, and gives you a selector for each:
✖ "compliancr" matches 2 units.
project compliancr ~/code/compliancr · compliancr 885624ef
stack compliancr cloudflare · 2 members 01a062e2
Re-run with project:compliancr or stack:compliancr.
A name that belongs to both a project and a stack is normal rather than exceptional: a Cloudflare-derived stack is named after the front-most worker in its cluster, so it shares that worker’s project name by design.
Daemon
The daemon is the background process that holds all running state. It starts automatically and stays alive until explicitly stopped.
corral daemon start
Start the daemon in the background. No-op if already running.
corral daemon stop
Stop the running daemon. Sends a graceful shutdown request, falls back to SIGTERM.
corral daemon status
Show whether the daemon is running, its PID, and the socket path.
Lifecycle
corral up
Start infrastructure (DNS + reverse proxy) and all auto-start projects.
corral up
corral down
Stop all running projects and infrastructure.
corral down
corral trust
Trust Corral’s local HTTPS root CA in the System keychain, so browsers —
Chrome included — accept .test over HTTPS. One-time, and it prompts for your
password. The Corral app’s Trust button does the same thing.
corral trust
corral status
Show every unit Corral manages. The table is NAME, KIND, STATE, HEALTH, LAST FAILURE — plus an ID column, but only when two rows share a name and you need a way to tell them apart.
STATE reads needs-setup for anything whose setup you still have to finish — the privileged helper waiting on your approval in Login Items, say. That is distinct from pending, which is a unit that is simply not started and wants nothing from you. LAST FAILURE carries failures only; a stalled setup is not one. Run corral status <unit> on a needs-setup row and it tells you what it is waiting on, and what to do about it.
corral status
corral status compliancr # detail for one unit
corral status stack:compliancr # when the name is shared
Failed units display structured failure details — failure kind, diagnostic message, and suggested remediation. Cascade failures (failures triggered by an upstream dependency) show a link to the root cause.
If the selector matches more than one unit, corral status prints all of them rather than choosing: it’s a read, so showing you both is more useful than refusing.
With --json and no argument, the output is a bare array sorted by name then id, each row {id, name, kind, state, health, last_failure}. state is one of pending, needs_setup, starting, initializing, running, reloading, stopping, stopped, failed, abandoned, or unknown — so a script can branch on needs_setup without reading prose. With a unit argument it is a single detail object; an ambiguous selector is an error there, carrying the candidates, because a script must not be handed one of two silently.
corral start <selector>
Start a project, a service or a stack.
corral start my-worker # a project
corral start stack:storefront # a stack
corral start service:postgres # a service
A bare name means the project when one carries that name — the same thing it has always meant. If a stack or service is named the same, Corral says so on stderr and tells you how to reach the other one; if nothing else shares it, it says nothing. The rule is about a shared name and nothing looser: a prefix that reaches two different things (web finding both web-admin and web-store) is a genuine ambiguity, and Corral asks rather than picking the project. corral stack start and corral service start remain, and keep their kind-specific options.
| Flag | Description |
|---|---|
--host | Serve on the local network (bind to 0.0.0.0 instead of 127.0.0.1). Projects only — a service or stack has no dev server to bind, and passing it is an error rather than a no-op |
--no-wait | Return as soon as the dev server has been spawned, without waiting to see whether it comes up. Projects only — a service or stack is driven to readiness by the daemon and already reports what it reached, so passing it is an error rather than a no-op |
When --host is used, the dev server becomes accessible from other devices on your local network. Corral prints the network URL after starting:
corral start my-worker --host
# ok Started my-worker
# network http://192.168.1.100:8787
It waits, and it tells you the truth
corral start follows the project until it settles, then reports what actually
happened — and exits non-zero if it did not come up.
That is a change. It used to print ok Started the moment the process had been
spawned, which says nothing: a dev server that crash-loops is given up on
about five seconds later, so ok Started was routinely the last thing a user
saw before a site that never served. A failure now prints the same structured
block corral status shows — including the last few lines the process actually
wrote before it died — and sets the exit status to match.
If it is still starting when the wait expires, Corral says that rather than
guessing either way, and exits 0. --no-wait restores the old fire-and-forget
behaviour.
Two outcomes are worth knowing by name, because they look alike and mean opposite things:
| What happened | What you see | Exit |
|---|---|---|
| The dev command ran to completion without ever serving — a build step or a one-shot script where a server was expected | exited before becoming ready, with the exit code, in about a second | non-zero |
Someone stopped the project while you were waiting — corral stop in another terminal, the app’s Stop button | stopped while starting, immediately | non-zero |
The first is a diagnosis: nothing is going to fix itself, so Corral does not retry it and does not call it a crash. The second is not a fault at all — the exit status is non-zero only because the command was asked to start something that is not now running.
corral up waits for its auto-start projects too, so their reported outcome
means “came up” rather than “was spawned”. Its exit status is unchanged — still
non-zero only when infrastructure fails, because one broken project among
several should not make a bulk command red forever.
corral stop <selector>
Stop a running project, service or stack.
corral stop my-worker
corral stop service:postgres
If the unit is a member of a running stack, Corral asks for confirmation first — stopping it would drop a member out from under a stack that still needs it. That now applies to services too, not just projects. Pass -y / --yes (or --force) to skip the prompt; non-interactive shells (pipes, --json) proceed without asking.
With --json, a stop of a service or a stack reports {"unit", "kind", "status"} — the kind is there because these verbs now drive three of them, and “which one did it act on” is exactly what a bare name leaves open. (Stopping a project previously reported {"project", "status"}; scripts reading .project should read .unit.)
corral restart <selector>
Stop and restart a project, service or stack.
corral restart my-worker
| Flag | Description |
|---|---|
--host | Serve on the local network (bind to 0.0.0.0 instead of 127.0.0.1). Projects only, as above |
--no-wait | Return as soon as the dev server has been spawned. Projects only, as above |
Like corral start, this waits for the project to settle and exits non-zero if
it does not come back up.
Recovering a failed project
There is no corral retry. corral start is the recovery path: it drives the
unit to running whatever state it is in, so a Failed or Abandoned project starts
fresh once you have fixed the underlying cause (freed a port, resolved a
dependency, made disk space).
corral start my-worker
Use restart for a running project that needs to pick up config changes. For
infrastructure, see corral infra retry.
corral share <project>
Share a project to the internet via a Cloudflare Quick Tunnel. Prints a public https://*.trycloudflare.com URL that anyone can access.
If the project isn’t running, Corral starts it first automatically.
corral share my-worker
# ok Sharing my-worker
# public https://random-words.trycloudflare.com
#
# This URL is ephemeral — it changes when the project restarts.
| Flag | Description |
|---|---|
--stop | Stop sharing instead of starting |
corral share my-worker --stop
# ok Stopped sharing my-worker
The tunnel URL is ephemeral — it changes every time the tunnel restarts. Sharing requires no Cloudflare account and is completely free. The cloudflared binary is downloaded automatically on first use (~30 MB).
Projects
corral project list
List all registered projects with their path, subdomain, auto-start status, and current state.
corral project list
corral project list --ungrouped # hide projects that belong to a stack
corral project add <path>
Register a project directory. Corral detects the wrangler config and parses it automatically.
corral project add ./my-worker
corral project add /Users/me/code/api-service
corral project remove <project>
Unregister a project. Does not delete any files.
corral project remove my-worker
corral project info <project>
Show detailed project information: ID, path, type, canonical URL, git repository and branch, bindings, settings, and running state. Git information (repository URL, host, and current branch) is included when the project has a detected git remote.
corral project info my-worker
corral project scan <directory>
Find all directories containing a recognized project (Cloudflare, Next.js, Vite, etc.).
corral project scan ~/code
corral project refresh <project>
Re-read the wrangler config from disk. Useful after editing wrangler.toml.
corral project refresh my-worker
corral project set <project> <key> <value>
Update a project setting.
corral project set my-worker node-version 22
corral project set my-worker subdomain api
corral project set my-worker environment staging
corral project set my-worker auto-start true
corral project set my-worker dev-command "npm run dev"
corral project set my-worker package-manager pnpm
corral project set my-worker address-mode localhost
corral project set my-worker localhost-port 5173
corral project set my-worker serve-proxy-route false
Available keys: node-version, subdomain, environment, auto-start, dev-command, package-manager, address-mode, localhost-port, serve-proxy-route
Clearing a value: use none for optional fields:
corral project set my-worker node-version none
Boolean values: true/false, yes/no, 1/0, on/off
Address mode: address-mode accepts proxy (serve <name>.test, the default), localhost (also serve http://localhost:<port>), or localhost-tls (also serve https://localhost:<port>). Pair it with localhost-port (a port number, or none to auto-pick) and serve-proxy-route false to serve localhost only. See Serving a project at localhost.
corral project open <project>
Open the project’s canonical URL in your browser — its address-mode URL (<name>.test by default, or http(s)://localhost:<port> when the project uses a localhost address mode).
corral project open my-worker
| Flag | Description |
|---|---|
--localhost | Open http://localhost:<port> instead of the .test domain (project must be running) |
--browser <name> | Open in a specific browser (e.g. “Google Chrome”) instead of the preferred/default |
corral project open my-worker --localhost
corral project open my-worker --browser "Firefox"
When --browser is not specified, Corral uses the preferred browser from settings. If no preferred browser is set, the system default is used.
corral project open-repo <project>
Open the project’s git repository in your browser.
corral project open-repo my-worker
| Flag | Description |
|---|---|
--browser <name> | Open in a specific browser (e.g. “Google Chrome”) |
corral project open-repo my-worker --browser "Safari"
The remote URL is detected when the project is added. If no git remote is found, the command will error and suggest running corral project refresh.
Stacks
A stack is a runnable composition of projects and services — start or stop the whole group with one command, and see how its members wire together. Reads (list / status) are free; creating, editing, and running stacks are Corral Pro.
Stacks are either manual (you choose the members) or Cloudflare-derived (membership and wiring are read from each worker’s wrangler config and kept in sync). Every corral stack command also accepts --json.
corral stack list
List every stack with its aggregate status, origin (manual / cloudflare), and member count.
corral stack list
corral stack status <stack>
Show a stack as a dependency tree — members with their lifecycle and dev URL, the wiring between them, and, for a Cloudflare-derived stack, whether its config has drifted (with a re-sync hint).
corral stack status storefront
corral stack create <name> [--member <project>…]
Create a manual stack, optionally seeding members.
corral stack create storefront --member web --member api
corral stack add <stack> <project>… · corral stack rm <stack> <project>
Add or remove members of a manual stack.
A Cloudflare-derived stack’s membership comes from wrangler config — edit the config, or corral stack eject it to a manual copy first (Corral will point you there).
corral stack add storefront auth
corral stack rm storefront auth
corral stack start|stop|restart <stack>
Bring the stack’s members up (in dependency order) or down.
Members are ref-counted: one shared by two running stacks isn’t stopped until both release it.
corral stack start storefront
corral stack stop storefront
corral stack resync <stack> · corral stack eject <stack>
Re-derive a Cloudflare-derived stack from its workers’ current wrangler config, or fork it into an editable manual copy (leaving the original intact).
corral stack resync storefront
corral stack eject storefront
corral stack delete <stack> · corral stack rename <stack> <new-name>
Delete a stack (its members are preserved) or rename it.
corral stack delete storefront
corral stack rename storefront shop
Node.js
corral node list
Show installed Node.js versions.
corral node list
corral node available
Show Node.js versions available for download (20 most recent).
corral node available
corral node available --lts
| Flag | Description |
|---|---|
--lts | Show only LTS releases |
corral node install <version>
Download and install a Node.js version.
corral node install 22.3.0
corral node install 22
corral node install lts
corral node remove <version>
Remove an installed Node.js version.
corral node remove 22.3.0
Command Execution
These commands are not Pro-gated, and must never be. They sit permanently on
your PATH via the shell shims, so
they run on every npm install whether or not you were thinking about Corral —
and refusing there would take out your toolchain over a license check.
Without Pro, the command is handed to your own binary with one line on stderr — and where you have no binary of your own, Corral runs the Node it manages instead, pins and all, because stepping aside onto nothing is a refusal wearing a passthrough’s clothes.
What Pro buys is Corral resolving the version in preference to a Node you
already have, plus an explicit --node-version. Installing the shims in the
first place is Pro (corral install-shims), and so is corral pm.
corral exec <command> [args...]
Run a command using the resolved Node.js version for the current directory.
corral exec node -v
corral exec npx vitest run
| Flag | Description |
|---|---|
--node-version <version> | Override version resolution |
Also respects the CORRAL_NODE_VERSION environment variable, which acts as the highest-priority pin rather than as an explicit request — see below.
corral npm [args...]
Shorthand for corral exec npm.
corral npm install
corral npm run build
corral npx [args...]
Shorthand for corral exec npx.
corral npx vitest
corral npx wrangler deploy
corral pm [args...]
Unlike exec, npm and npx, this one genuinely requires Pro: cpm shadows no
system binary, so there is nothing to hand off to when it can’t help.
Universal package manager command. Resolves the correct package manager for your project (npm, yarn, pnpm, or bun) and translates your command into the right syntax.
corral pm install
corral pm add react
corral pm add -D vitest
corral pm remove lodash
corral pm update
corral pm update react
corral pm run dev
corral pm test
update (or upgrade) is the one worth pointing at: every manager spells it
differently — npm update, pnpm update, bun update, yarn upgrade, and
yarn up -R on Yarn Berry — and bun upgrade upgrades Bun itself rather than
anything in your project. Corral picks the right one, within the ranges your
package.json declares.
A verb Corral doesn’t recognise is passed through untouched, and so is anything
carrying a flag (corral pm update --latest runs your manager’s own update --latest). up is passed through on purpose: npm and pnpm treat it as
update, but on Yarn Berry it means “ignore my declared ranges”, and Corral
won’t quietly decide which one you meant.
| Flag | Description |
|---|---|
--node-version <version> | Override Node.js version resolution |
--about | Show which package manager would be used and why |
Resolution order: Which package manager Corral uses has the chain, in order, with what sets each tier.
It is not specific to corral pm. The dev server Corral starts for a project
and the app’s Dependencies tab resolve the same way — which is why the order
lives in one place and every other page links to it. A copy here would be a
copy to keep in step, and the last one was four tiers out of date.
Bidirectional syntax: Both npm-style and yarn/pnpm-style commands work regardless of the underlying package manager:
# These do the same thing — add a dependency
corral pm install react # npm style
corral pm add react # yarn/pnpm style
# These do the same thing — remove a dependency
corral pm uninstall lodash # npm style
corral pm remove lodash # yarn/pnpm style
Every invocation prints the resolved package manager to stderr (e.g., ▸ pnpm) so you always know what’s running.
Diagnostics: Use --about to see the full resolution chain:
corral pm --about
# Package manager pnpm
# Source package.json
# Project my-api
# Project override (not set)
# package.json pnpm@9.4.0
# App default npm
# Node.js v22.14.0
Source is the reason, not a filename: a lockfile answer names the lockfile,
a declaration names the file that declared it, and an override says so. Every
declaration Corral read gets its own row, and a row it could not honour — a
package manager Corral does not run, a release it refused — carries a line
saying why underneath. No version is compared here: corral pm runs in your
terminal, and which binary the daemon would run is a question only the
daemon can answer, so the Dependencies tab is where that lives.
Shell Shims
corral install-shims
Install node, npm, and npx shims to ~/.corral/shims/, and cpm to ~/.corral/bin/. A fourth file, ~/.corral/libexec/corral-shim-fallback, is written alongside them — it’s what the shims hand off to if Corral’s CLI ever goes missing.
The two directories are separate on purpose: the first three shadow commands you may already have, and cpm doesn’t. See Node.js Management — Shell Shims.
corral install-shims
| Flag | Description |
|---|---|
--patch-profile | Add both directories to PATH in your shell profile |
corral uninstall-shims
Remove shims and undo shell profile changes.
corral uninstall-shims
Uninstall
corral uninstall
Completely remove Corral and all its data from macOS. This performs a full cleanup:
- Removes the CA certificate from the system keychain
- Kills any orphaned Caddy and dnsmasq processes
- Removes the DNS resolver file (
/etc/resolver/{tld}) - Removes the privileged helper daemon
- Deletes the data directory (
~/.corral) - Clears app preferences
- Removes shell profile shims
- Removes the installed CLI (
/Library/Application Support/Corral/bin/corral), its/usr/local/bin/corralentry, and any~/.corral/bin/corralcopy - Moves
Corral.appto the Trash — so a removal is recoverable for as long as you haven’t emptied it
corral uninstall
You will be prompted for confirmation before anything is removed. Use --force to skip the prompt:
corral uninstall --force
Use --keep-data to remove Corral but keep ~/.corral — your projects, settings, license, installed Node.js versions and service data:
corral uninstall --keep-data
Corral’s shims and binaries inside that directory (shims/, bin/, libexec/) are removed either way, along with the shell-profile block, so nothing is left on your PATH pointing at an app that’s gone. This is the same choice the macOS app’s Remove Corral completely… sheet offers as a checkbox.
| Flag | Description |
|---|---|
--force | Skip the confirmation prompt |
--keep-data | Keep ~/.corral (minus Corral’s shims and binaries) |
When a removal doesn’t finish
Some of the cleanup needs administrator access, and some of it only the app itself can do — so a removal can end with the machine part-way. It says which, and a script can tell the two apart.
With --json the result is {removed, warnings, incomplete_because, helper_cleared, kept_data, app_bundles_left}. removed is an observation, not a constant: it is true only when the privileged helper was cleared and no copy of Corral.app is still on disk. app_bundles_left names the ones that are. incomplete_because is null on a clean run, and otherwise one of:
| Value | What happened | What fixes it |
|---|---|---|
root_unavailable | Something needed administrator access and couldn’t get it, so the system keychain certificate, the DNS resolver file or the root-owned CLI are still there | Re-run corral uninstall from a terminal, where it can ask |
helper_registered | The privileged helper’s entry under Login Items & Extensions → App Background Activity survived, and only the app that registered it can clear it | A later run, or the manual step the output names. The app is deliberately kept so there is something left to clear it with |
other | Something else | The step’s own line in the output |
corral uninstall exits non-zero only for root_unavailable — the one a person can act on right now. The other two describe a machine this command exists to be run on, and exiting non-zero for them would make its designed behaviour look like a fault. So a script that just wants “did it finish” can read $?, and one that wants to know why reads incomplete_because.
Update
corral update
Check for and install the latest version.
corral update
| Flag | Description |
|---|---|
--check | Check for updates without installing |
corral update --check
What it updates depends on where your corral came from, and you don’t have to choose — it works it out.
If Corral installed it (the usual case: the app is in /Applications and corral is on your PATH from there), the CLI and the app are one release, so updating one means updating both. The command reinstalls the app bundle and then asks the running daemon to refresh the installed CLI. That last step goes through the daemon rather than sudo, so a normal update never prompts you for a password.
If the daemon isn’t running, or you haven’t approved the privileged helper yet, the app still updates and the command still succeeds — it prints a note asking you to open Corral so the system-wide corral can catch up.
A corral you downloaded and put somewhere yourself updates only itself, and says so:
note: Updating this copy only (/Users/you/bin/corral). Your Corral.app install updates separately.
Install
corral install app
(Re)install or repair the macOS app from the terminal. The standard installer already sets the app up; reach for this if the app bundle was removed, or to force a fresh, fully-verified copy. It downloads Corral.app to /Applications through the same two-layer-verified path the in-app updater uses.
corral install app
| Flag | Description |
|---|---|
--force | Reinstall even if the app is already present |
--no-cli | Install the app only, leaving any existing corral untouched |
--cli-dir <DIR> | Copy corral into an extra directory of your choosing, as well as the usual one |
--require-cli | Fail if the CLI can’t be installed anywhere, instead of warning |
After installing, the corral CLI is refreshed from the new bundle.
The CLI is always installed to ~/.corral/bin/corral, with your shell profile patched to put it on PATH. This command never writes /usr/local/bin — that path is machine-wide while this install is yours alone, so it belongs to the privileged helper, which points it at the root-owned copy on the app’s first launch.
Logs
corral logs [selector]
View logs for any unit corral status lists — a project, a service, an
infrastructure unit. Omit the selector to see everything merged.
corral logs
corral logs my-worker
corral logs postgres # a service
corral logs caddy # the reverse proxy
A stack means the logs of everything it starts — its members, what those
members depend on, and the same again through nested stacks. That is the same
set corral stack start brings up and corral stack status draws, so the
Postgres a worker needs is in view rather than being the one thing missing when
the stack is broken. Corral prints which units it widened to on stderr; every
line is labelled with the unit it came from regardless.
Both of the notes corral logs can print — the one naming a stack’s sources,
and the one saying a shared name was read as the project — go to stderr, and
--json does not silence them. --json describes stdout, which stays clean for
whatever is consuming it; a command that read a different unit than you named
says so either way.
corral logs storefront --tail 40
| Flag | Short | Description |
|---|---|---|
--follow | -f | Stream logs in real-time |
--tail <n> | -t | Show only the last N entries (default: 50 in follow mode) |
--search <query> | -s | Filter by text content (case-insensitive) |
--level <level> | Minimum severity: trace, debug, info, warn, error, fatal | |
--since <ts> | Inclusive lower bound, as an RFC3339 timestamp | |
--until <ts> | Exclusive upper bound, as an RFC3339 timestamp | |
--context <kind> | Filter by context kind: lifecycle, error, progress, queue | |
--snapshot <id> | Read one failure snapshot — its id, a unique id prefix, or a <selector>:<iso8601> deep-link (see below) |
Examples:
# Stream all logs
corral logs --follow
# Last 10 entries from one unit, then stream
corral logs my-worker -f -t 10
# Search across all logs
corral logs --search "error"
corral logs snapshots [selector]
When a unit fails on its own account, and the way it failed could have produced logs worth keeping, Corral captures a snapshot of its recent log entries and keeps it. This lists them — for one unit, or for everything if you omit the selector.
corral logs snapshots
corral logs snapshots my-worker
ID UNIT WHEN FAILURE ORIGIN
3f2a1b9c my-worker 2026-05-01 10:04:11 ExitedBeforeReady own
8b41d0e2 my-worker 2026-05-01 09:58:02 Crashed inherited
ORIGIN says whether the unit failed on its own (own) or was handed a copy
of something upstream that failed (inherited) — those mean different things
when you are working out what broke.
On an inherited row, FAILURE is the root cause’s kind, not this unit’s.
The second row above is my-worker’s copy of a snapshot taken when the thing it
depends on crashed; my-worker’s own failure was a DependencyDown, which has
no logs of its own to capture. That is the point of the copy — it tells you what
actually broke, and it keeps the root’s snapshot id too, so both units’ rows
lead to the same forensic.
The ID column prints the shortest prefix that is unambiguous among the rows
shown, and --snapshot accepts exactly that back.
Reading one snapshot
corral logs --snapshot 3f2a1b9c
corral logs --snapshot my-worker:2026-05-01T10:04:11Z
Either a snapshot id (or a unique prefix of one — this is what corral status
prints under a failure), or a <selector>:<timestamp> deep-link. --search
filters within the snapshot.
If a project is literally named snapshots, reach it with
corral logs project:snapshots — the subcommand wins over a bare name.
Settings
corral settings show
Display current app settings.
corral settings show
corral settings set <key> <value>
Update a setting.
corral settings set tld localhost
corral settings set auto-start true
corral settings set default-node-version 22
corral settings set port-range-start 9000
corral settings set default-package-manager pnpm
corral settings set preferred-browser com.google.Chrome
corral settings set serve-at-login false
corral settings set lazy-start-default false
Available keys: tld, auto-start, default-node-version, port-range-start, default-package-manager, preferred-browser, serve-at-login, lazy-start-default
Clear optional settings with none:
corral settings set default-node-version none
Infrastructure
corral infra start
Start DNS and proxy services.
corral infra start
corral infra stop
Stop DNS and proxy services.
corral infra stop
corral infra retry
Recover failed infrastructure: drives the system helper, DNS and the proxy back to running. It retries failed or abandoned units, starts stopped ones, and leaves healthy ones untouched — so it is safe to run when you are not sure which part is broken.
corral infra retry
corral infra status
Show infrastructure state.
corral infra status
Services
corral service list
List installed services and their status (name, version, ports, state, auto-start). Services with more than one port render all of them in the PORTS column as role=port pairs (e.g. smtp=1025,web=8025).
corral service catalog
Show available services from the remote catalog, grouped by category.
corral service install <name>
Install a service. Downloads the binary, verifies the checksum, and extracts it.
Services that support concurrent multi-version installs (e.g. postgres, mariadb) accept a name@version argument and install side-by-side with any other installed majors; each gets its own port and its own isolated data directory. The version component can be exact (postgres@16.4.1, mariadb@10.11.16) or a prefix (postgres@16 resolves to the newest 16.x; mariadb@11.4 resolves to the newest 11.4.x). Bare name resolves to the catalog’s default version for the install.
LTS-aware default-version resolution. For services with multiple LTS branches in flight (mariadb today: 10.11, 11.4, 11.8 LTS plus rolling 12.x), the bare-name install resolves to the newest LTS rather than the newest version overall — corral service install mariadb lands on the latest 11.8.x (or whichever LTS is currently freshest), not on a rolling 12.x release. This is per-service catalog metadata (lts_branches + default_version_policy), so it doesn’t change behavior for services like postgres where every supported major is itself a long-term branch.
| Flag | Description |
|---|---|
--version <version> | Legacy equivalent of the @<version> suffix; accepts the same exact/prefix forms. |
corral service uninstall <name>
Uninstall a service. Removes the binary but keeps the data directory by default. For multi-version services, target a specific install with name@version (e.g. corral service uninstall postgres@15) — other installed versions are untouched.
| Flag | Description |
|---|---|
--delete-data | Also remove the service’s data directory |
corral service start <name>
Start a stopped service. Accepts name@version to target a specific installed version of a multi-version service.
corral service stop <name>
Stop a running service. Accepts name@version to target a specific installed version of a multi-version service.
corral service restart <name>
Restart a service (stop then start). Accepts name@version to target a specific installed version of a multi-version service.
corral service info <name>
Show service details: version, port(s), status, connection string, environment variables, web UI URL (if any), auto-start, data and install paths. Accepts name@version.
If a bare name is passed for a service with multiple installed versions and no default-version set, the command errors with AmbiguousVersion and lists the installed candidates.
corral service set <name> <key> <value>
Update a service setting.
| Key | Values | Description |
|---|---|---|
port | Number >= 1024 | Port the service listens on. Only valid for single-role services whose sole role is main (e.g. Valkey, Meilisearch). |
port.<role> | Number >= 1024 | Port for a specific role on any service with named roles: port.smtp 1026 / port.web 8030 (Mailpit), port.sql 5433 (PostgreSQL), port.sql 3307 (MariaDB — same sql role name as Postgres), port.http 9201 / port.transport 9301 (OpenSearch), port.api 9100 / port.console 9101 (MinIO), port.api 8200 / port.peering 8201 (Typesense). Each port must be >= 1024 and unique across roles. |
auto-start | true/false | Start automatically with the daemon |
default-version | Version string, or - to unset | For services with concurrent multi-version installs, the version resolved by bare-name lookups (e.g. corral service start postgres). - unsets, after which bare names fail with AmbiguousVersion when 2+ versions are installed. |
JSON output
corral service list --json and corral service info --json emit a RunningServiceInfo record per service:
{
"name": "mailpit",
"display_name": "Mailpit",
"version": "1.29.7",
"service_ref": "mailpit@1.29.7",
"is_default": true,
"ports": {"smtp": 1025, "web": 8025},
"state": "Running",
"auto_start": false,
"connection_string": "smtp://127.0.0.1:1025",
"env_vars": [
{"key": "SMTP_HOST", "value": "127.0.0.1"},
{"key": "SMTP_PORT", "value": "1025"},
{"key": "SMTP_FROM", "value": "corral@localhost"}
],
"web_ui_url": "http://127.0.0.1:8025",
"data_dir": "/Users/you/.corral/services/mailpit/1.29.7/data",
"install_dir": "/Users/you/.corral/services/mailpit/1.29.7"
}
For services with multiple installed versions, list emits one record per (service, version) pair — each with its own service_ref and is_default flag. Example for a machine that has Postgres 16 and 17 side-by-side with 16 marked default:
[
{
"name": "postgres",
"display_name": "PostgreSQL",
"version": "16.4.1",
"service_ref": "postgres@16.4.1",
"is_default": true,
"ports": {"sql": 5432},
"state": "Running",
"auto_start": true,
"connection_string": "postgresql://corral@127.0.0.1:5432/corral",
"env_vars": [
{"key": "DATABASE_URL", "value": "postgresql://corral@127.0.0.1:5432/corral"},
{"key": "PGHOST", "value": "127.0.0.1"},
{"key": "PGPORT", "value": "5432"},
{"key": "PGUSER", "value": "corral"},
{"key": "PGDATABASE", "value": "corral"}
],
"web_ui_url": null,
"data_dir": "/Users/you/.corral/services/postgres/16.4.1/data",
"install_dir": "/Users/you/.corral/services/postgres/16.4.1"
},
{
"name": "postgres",
"display_name": "PostgreSQL",
"version": "17.2.0",
"service_ref": "postgres@17.2.0",
"is_default": false,
"ports": {"sql": 5433},
"state": "Stopped",
"auto_start": false,
"connection_string": "postgresql://corral@127.0.0.1:5433/corral",
"env_vars": [],
"web_ui_url": null,
"data_dir": "/Users/you/.corral/services/postgres/17.2.0/data",
"install_dir": "/Users/you/.corral/services/postgres/17.2.0"
}
]
service_ref is the canonical name@version string — pass it straight back into any command that accepts name@version. is_default is the marker set by corral service set <name> default-version <version>, useful for scripting a “start the default” flow without having to re-read the registry.
Data Inspection
Every command in this section is Pro except the first: corral data bindings
is free, because knowing which bindings a project declares is part of seeing your
project at all. Reading what is inside them is Pro.
corral data bindings <project>
Show a summary of all bindings and their local data status.
corral data bindings my-worker
corral data d1 tables <project> <binding>
List tables in a D1 database.
corral data d1 tables my-worker MY_DB
corral data d1 query <project> <binding> <table>
Query rows from a D1 table.
corral data d1 query my-worker MY_DB users
corral data d1 query my-worker MY_DB users --limit 20 --offset 40
| Flag | Default | Description |
|---|---|---|
--limit | 50 | Maximum rows to return |
--offset | 0 | Row offset for pagination |
corral data d1 schema <project> <binding> <table>
Show column schema for a D1 table.
corral data d1 schema my-worker MY_DB users
Displays column name, type, nullable, default value, and primary key status.
corral data kv keys <project> <binding>
List keys in a KV namespace.
corral data kv keys my-worker MY_KV
corral data kv keys my-worker MY_KV --prefix user:
| Flag | Default | Description |
|---|---|---|
--prefix | (none) | Filter keys by prefix |
--limit | 50 | Maximum keys to return |
--offset | 0 | Key offset for pagination |
corral data kv get <project> <binding> <key>
Get a KV entry’s value.
corral data kv get my-worker MY_KV user:123
corral data r2 objects <project> <binding>
List objects in an R2 bucket.
corral data r2 objects my-worker MY_BUCKET
corral data r2 objects my-worker MY_BUCKET --prefix uploads/
| Flag | Default | Description |
|---|---|---|
--prefix | (none) | Filter objects by prefix |
--limit | 50 | Maximum objects to return |
--offset | 0 | Object offset for pagination |
corral data r2 get <project> <binding> <key>
Download an R2 object. By default, writes raw bytes to stdout (suitable for piping). Use --output to save to a file.
corral data r2 get my-worker MY_BUCKET image.png --output ./image.png
corral data r2 get my-worker MY_BUCKET config.json > config.json
| Flag | Description |
|---|---|
--output, -o | Write to a file instead of stdout |
corral data do instances <project> <binding>
List Durable Object instances.
corral data do instances my-worker MY_DO
A NAME column appears when at least one instance has a name — the argument your Worker passed to idFromName(...), which Miniflare records on disk. Unnamed instances in a named list show a dimmed dash. When nothing is named the column is omitted entirely, so a Worker using newUniqueId() throughout doesn’t pay a column of dashes for nothing.
With --json, name is always present and may be null, so a script can count on the key. It carries the raw recorded value rather than the trimmed one shown in the table, because only the raw value round-trips back through idFromName(...) to the same instance.
corral data do storage <project> <binding> <instance_id>
List storage entries for a Durable Object instance.
corral data do storage my-worker MY_DO abc123def456
corral data do storage my-worker MY_DO abc123def456 --limit 20 --offset 0
| Flag | Default | Description |
|---|---|---|
--limit | 50 | Maximum entries to return |
--offset | 0 | Entry offset for pagination |
corral data secrets list <project> <binding>
List secrets in a Secrets Store binding.
corral data secrets list my-worker API_KEY
| Flag | Default | Description |
|---|---|---|
--limit | 50 | Maximum secrets to return |
--offset | 0 | Secret offset for pagination |
corral data secrets get <project> <binding> <name>
Get a secret’s value.
corral data secrets get my-worker API_KEY my-api-key
corral data secrets create <project> <binding> <name>
Create a secret. The value is read from stdin — in an interactive terminal you will be prompted, or you can pipe it:
corral data secrets create my-worker API_KEY my-api-key
echo "sk-1234" | corral data secrets create my-worker API_KEY my-api-key
corral data secrets delete <project> <binding> <name>
Delete a secret.
corral data secrets delete my-worker API_KEY my-api-key
corral data workflow instances <project> <binding>
List workflow instances with status, step count, and size.
corral data workflow instances my-worker MY_WORKFLOW
corral data workflow detail <project> <binding> <instance_id>
Show details for a workflow instance including steps, params, and status.
corral data workflow detail my-worker MY_WORKFLOW abc123def456
License
corral license status
Show the current license status.
corral license status
Displays license status, customer info, device count, expiry date, and validation state. Shows “No license activated” if no license is present.
Two separate offline limits are surfaced when they apply: validation retries (attempts remaining after failed check-ins) and offline (days before the signed proof goes stale). See Subscription for how they differ.
corral license activate <key>
Activate a Corral Pro license key on this device.
corral license activate 90F84D88-5A29-4AB4-8B27-E93602B0F274
Activates the license with the license server, stores the activation locally, and unlocks Pro features immediately.
corral license refresh
Force a check-in with the licensing server now.
corral license refresh
Corral normally revalidates every 24 hours, but only while it’s running. If the app has been closed long enough for the signed proof of your license to go stale, this is the fix — it ignores the 24-hour window and validates immediately. Works with or without Corral running.
It exits non-zero when the check-in didn’t happen — the server was unreachable, rate-limited, or answered in a way Corral couldn’t verify — and says which. That last one covers the case you’ll actually meet: a hotel or office captive portal answering with its own sign-in page instead of the licensing server. Corral treats that as “no answer”, not as a verdict. Success means the round trip produced an actual verdict, including a verdict you won’t like: “your license has expired” is a successful refresh. So a zero exit means the state on disk is the server’s current answer, which is what a script wants to know.
An inconclusive attempt never changes your entitlement — nothing was learned, so nothing is concluded — and it’s always safe to retry. It does spend one of the grace attempts when the server never answered, the same as a scheduled check-in that couldn’t reach it. Replies that are answers — a rate-limit, or a response Corral couldn’t verify — don’t, because the server demonstrably looked.
corral license deactivate
Deactivate the license on this device, freeing the device slot.
corral license deactivate
Deactivates the license with the license server and removes the local license file. Use this before transferring your license to another device.
The macOS app’s Subscription room provides a visual interface for all of the above (status, activate, deactivate, retry validation, manage subscription).
MCP
Corral ships an MCP (Model Context Protocol) server so AI coding tools — Claude Code, Cursor, VS Code, Codex, Continue, Zed, Windsurf — can drive your local environment over a typed protocol. The server is built into the CLI; the subcommands below detect and configure those clients. See AI Tools (MCP) for the full guide.
corral mcp
Start the stdio MCP server. AI clients spawn this automatically — you don’t run it by hand. corral mcp serve is the explicit spelling of the same thing.
corral mcp install
Configure AI clients to use Corral’s MCP server. With no --client, configures every client detected on your machine.
corral mcp install # every detected client, user scope
corral mcp install --client cursor # one client (honored even if not yet installed)
corral mcp install --project # project-scoped config in the current directory
| Flag | Description |
|---|---|
--client <name> | Configure only this client: claude-code, cursor, vscode, codex, continue, zed, windsurf |
--project | Write project-scoped config (current directory) instead of user-global config |
Corral merges a single corral entry into each client’s native config (JSON, TOML, or YAML), leaving everything else untouched. The command exits non-zero if any targeted client could not be configured.
corral mcp uninstall
Remove Corral’s entry from AI client configs — other servers and settings are left untouched. Same --client / --project flags as install; with no --client, removes from every detected client.
corral mcp uninstall
corral mcp uninstall --client zed
corral mcp status
Show each AI client’s configuration status at every scope it supports: configured, not configured, mismatched (points at a different path), unparseable, or error (the config couldn’t be read). Supports --json.
corral mcp status
Shell Completions
corral completions [<shell>] [--patch-profile]
Set up shell completion. Supported shells: bash, zsh, fish, elvish, powershell. With no shell named, Corral uses the one you’re running.
corral completions --patch-profile # set it up in your shell profile
corral completions zsh # or print the line and place it yourself
corral completions --remove # take it back out
Completion is live: corral start compl<TAB> offers the projects, services and stacks your daemon actually knows about, not just subcommands and flags. Two things that share a name complete to whatever tells them apart — two projects called api offer their subdomains, api and api-2 — because a shell can only substitute a value that continues what you typed.
With no daemon running, completion offers nothing rather than starting one or printing an error at you.
Why it’s a source line, not a file. The line Corral adds regenerates the completion script each time a shell starts:
command -v corral >/dev/null 2>&1 && (( $+functions[compdef] )) && source <(COMPLETE=zsh corral)
That script and the corral binary are one interface, and it has no stability guarantee across versions — a copy saved to a file goes stale the next time Corral updates, silently, into either wrong completions or none at all. Regenerating on each shell start is self-correcting. It costs about 10ms.
The guards matter as much as the line: command -v corral keeps the line silent if Corral is ever removed, and $+functions[compdef] keeps it silent in a zsh that never runs compinit. corral doctor tells you if that second one is why completion isn’t working.
The line lives in the same Corral-managed block as the shims’ PATH entry, between # >>> corral and # <<< corral, so corral uninstall removes it and nothing is left behind. corral uninstall-shims takes out the PATH line and leaves completion alone.
fish, elvish and powershell print the line to add rather than editing a startup file, since Corral doesn’t manage those.
Diagnostics
corral doctor
Run a system health check. Verifies that all infrastructure components are configured and responding.
corral doctor
Checks performed:
| Check | What it verifies |
|---|---|
| Daemon | Background daemon is running |
| System helper | The privileged helper is installed and answering. When a helper process is running but not responding — the classic case after Corral updates — it says so and gives you the launchctl kickstart command that fixes it. When no helper process is running it can’t tell a crashed helper from one that was never installed, so it reports the install path. Warns rather than fails when the daemon is down, because without it the helper’s registration can’t be read. If Corral.app isn’t on the Mac at all, the remedy becomes corral install app rather than pointing at an app you haven’t got |
| DNS resolver | /etc/resolver/{tld} file exists |
| dnsmasq | DNS server is responding on 127.0.0.1 |
| Caddy | Reverse proxy is responding |
| Node.js | You can run node here. Passes when Corral manages a version, and also when it manages none but your system has one — naming which. Only a machine with no Node at all fails |
| Shims | Shell shims are installed, in the current layout, and on this shell’s PATH. Tier-aware: shims are a Pro feature, so a free-tier machine without them is in its correct state and passes |
| Completions | Shell completion is set up, and reachable when your profile runs it |
| Login agent | Corral’s services are registered to start at login, and the binary that registration names is still there. Passes when both hold; the two broken shapes warn rather than fail, because every command starts the daemon on demand |
| Toolchain env | The environment Corral resolved for your project tooling. It reads your login shell once at start and uses that PATH for everything it runs, so a Homebrew or version-manager tool is found whether Corral was opened from the Dock or from a terminal. Asked over the daemon on purpose: doctor runs in your shell, where $PATH is already the working one, so it could never see the difference. Warns rather than fails when the shell can’t be read — Corral falls back to the environment it was started with, which is what it always did |
| License | Your entitlement, and what to do about it — the same states the Subscription room shows. A free-tier install passes; it isn’t sold to |
One more check appears only when it applies:
| Check | When it appears |
|---|---|
| Shims vs Pro | Shims are installed and active, but Pro isn’t. Names which of the three things is actually happening: passing through to your system versions, running the Node Corral manages (resolved for the current project, exactly as it would be for a Pro user), or finding no Node at all |
So corral doctor reports eleven checks normally, and twelve when that last one fires.
Each check reports pass, warn, or fail with an actionable message. Use --json for machine-readable output; a warning counts as passing there, so existing scripts keep working.
| Flag | Description |
|---|---|
--json | Machine-readable output, including a passed boolean |
--strict | Exit non-zero if any check fails |
corral doctor exits 0 whatever it finds, so that a health check in a script
never breaks the script. --strict is the opposite contract, for when the check
is the gate: it exits 1 as soon as one check fails.
Warnings still count as passing under --strict, matching --json’s passed
field — the two must agree, or the same install is healthy through one flag and
broken through the other. A warning means “this works, and here is something to
know”; the shim rows are the common case.
The shims row distinguishes states that need different fixes, rather than calling all of them “not in PATH”:
| Result | Meaning |
|---|---|
| pass | Installed in ~/.corral/shims/ and on this shell’s PATH. |
| warn — old location | Still in the pre-shims/ layout. They work; Corral moves them on its next start. See upgrading from an older Corral. |
| warn — shell predates the profile | Your profile is configured correctly and this terminal started before that. Open a new one, or run exec $SHELL. Re-running the installer would change nothing. |
| fail — not installed | Pro only. Run corral install-shims --patch-profile. |
| fail — not in PATH | Pro only. Installed, but nothing puts them on PATH. Same command. |
| pass — free tier, not installed | Nothing is wrong. Shims are a Pro feature, so their absence is the correct state and the row says what they are rather than how to repair something. |
| warn — free tier, stranded | Installed (from a previous Pro period) but not on PATH. A warning, not a failure: the command that would fix it is one this tier cannot run. |
A check may only report fail when you can act on it at your tier. Before
that rule, a healthy free-tier machine failed this row with a Pro-only remedy,
which made corral doctor --strict impossible to satisfy — and a gate that is
red on a healthy machine gets ignored, which is the thing --strict exists to
prevent.
corral report
File a bug report with the maintainers, optionally attaching a diagnostics bundle (logs, unit state, crash reports). Runs entirely in-process, so it works even when the daemon is wedged or won’t start. See Reporting Problems for what’s included and the privacy posture.
corral report
corral report -m "wrangler dev exits immediately" --include-logs
corral report -m "DNS stopped resolving .test" --unit dns -y
Run with no -m in a terminal and Corral opens $EDITOR for the description, then prompts for diagnostics, a contact email (pre-filled from your license), and a confirmation. Off a terminal — or with --json — diagnostics are opt-in: they’re never bundled unless you pass --include-logs or --save.
| Flag | Short | Description |
|---|---|---|
--message <text> | -m | The problem description. If omitted on a terminal, opens $EDITOR. |
--email <email> | Contact email for follow-up (pre-filled from your license if present). | |
--include-logs | Include diagnostics (logs, unit state, crash reports). Prompted on a terminal; off a terminal it’s off unless you pass this. | |
--no-logs | Do not include diagnostics (message only). | |
--unit <selector> | Attach a specific unit’s failure snapshot. Takes the full selector grammar — a name, kind:name, a subdomain, a uuid, or an id prefix. | |
--yes | -y | Skip the confirmation prompt. |
--dry-run | Build the diagnostics bundle but do not send it. | |
--save <path> | Write the diagnostics bundle to this path (implies --include-logs). |
On success, Corral prints a reference code:
corral report -m "..." -y
# ✓ Sent — reference CORRAL-7F3A2
# Keep this reference if you'd like to follow up.
With --json, the result is one of these shapes:
{"status":"sent","reference":"CORRAL-7F3A2","included_logs":true,"bundle_bytes":52124}
{"status":"not_sent","saved":"/path/to/report.tar.gz","included_logs":true,"bundle_bytes":52124}
{"status":"network_failure","message":"...","saved":"/path/to/report.tar.gz","included_logs":true}
{"status":"rejected","http_status":429,"message":"..."}
{"status":"cancelled"}