Skip to content

CLI Reference ​

The portbay CLI shares the same portbay_lib core as the Tauri GUI. It acts as a client: lifecycle commands (start, stop, restart) require the PortBay daemon (the GUI app, or a future portbay daemon subcommand) to be running and exposing Process Compose on a discoverable port.

Typed without a management subcommand, portbay starts the local PortBay agent engine, similar to codex or claude:

bash
portbay
portbay "inspect this repo and summarize the next fix"
portbay --json -P ollama -m qwen2.5-coder:latest "run the task"

The command is lowercase on disk and case-insensitive wherever PortBay parses it as an agent id.

Install ​

The app ships the CLI inside its bundle. To put it on $PATH, open Settings → Advanced and enable the Command-line tool row — this symlinks the bundled binary to /usr/local/bin/portbay (one OS authorization prompt at most, VS Code's "install code command" model). The agent engine ships in the same bundle and is resolved through that symlink automatically; no second install step is needed. Homebrew installs (brew install portbay-app/portbay/portbay) land on $PATH directly.

Global options:

OptionMeaning
--jsonEmit machine-readable JSON.
--registry <PATH>Override the registry file location.
--pc-port <PORT>Override the Process Compose daemon port.

Commands ​

CommandPurpose
portbay listList registered projects with live status when the daemon is reachable.
portbay status [id]Show one project's status, or all projects when no id is provided.
portbay add <PATH>Register a project from a folder path. Auto-detects .portbay.json if present.
portbay remove <id>Unregister a project and remove generated artifacts by default.
portbay update <id>Change a project's start command, environment or kind.
portbay start <id>Start one project.
portbay stop <id>Stop one project.
portbay stop --allStop every running process.
portbay restart <id>Restart one project.
portbay logs <id>Print static log output for a project.
portbay runtime pin <id> <lang@version>Pin one project's runtime version, or clear it with --clear.
portbay open <id>Open the project URL in the default browser.
portbay doctorGrouped, flutter doctor-style health check across registry, daemon, routing/TLS, PHP, services, and account.
portbay hosts <subcommand>Manage PortBay's /etc/hosts block.
portbay export <id>Write <project_path>/.portbay.json.
portbay completions <shell>Generate shell completion scripts.
portbay login [--email <addr> | --device]Sign in to PortBay Cloud (GitHub OAuth by default; --email for a magic link; --device for a short code typed on another device).
portbay licenseShow the current account, tier, and entitlement limits.
portbay logoutSign out and clear the saved session.
portbay token create|list|revokeManage account tokens for CI (PORTBAY_TOKEN).

Agent mode — retired ​

portbay used to double as an agent CLI: anything that wasn't a management subcommand was handed to a bundled portbay-agent engine, so portbay "fix the failing test", portbay -p "plan the refactor" and portbay --json "run the task" all started an agent run.

That engine no longer ships. PortBay runs agent work on its own native in-process runtime instead of bundling a second engine. -p is PortBay's own one-shot now (see below); a bare prompt or an engine flag prints what happened and exits 2 rather than doing something else quietly:

$ portbay "plan the refactor"
portbay: `plan the refactor` is not a PortBay command.

The bundled `portbay-agent` engine that used to run free-text prompts and
agent flags typed at `portbay` has been retired. PortBay runs agent work on
its own native runtime now instead of shipping a second engine, and there is
no CLI entry point that takes a bare prompt.

  portbay -p "<prompt>"         run one prompt in this project and exit
  portbay                       open the PortBay terminal
  portbay tui --project <id> --card <id>
                                run a board card on the native runtime
  portbay tasks list <project>  read the board from the shell
  portbay --help                every command this build has

What replaces each old use:

WasNow
portbay (interactive session)portbay still opens PortBay's own terminal.
portbay "<prompt>" / -iportbay -p "<prompt>" for one answer, or put the work on the board and dispatch the card — portbay tui --project <id> --card <id> runs it on the native runtime.
portbay --json "<prompt>" (headless NDJSON)portbay -p "<prompt>" --output-format stream-json. The old agent_event / run_result NDJSON contract belonged to the retired engine; the stream is tasks run --json's.
PORTBAY_AGENT_BINNothing. No code path resolves an agent sidecar any more.

Local-model board dispatches are unaffected in the place that matters: an Ollama or Cloud (BYOK) card still runs a full agentic read/edit/run loop, on the native runtime rather than through the bundled engine.

-p — one prompt, no terminal ​

portbay -p "<prompt>" runs one turn of a terminal session in the project the current directory belongs to, prints the answer and exits. With no value the prompt is read from stdin (git diff | portbay -p). Nothing is written to the board.

Flag
--output-format text|json|stream-jsontext (default) prints the answer alone; json one result object; stream-json every event as NDJSON with "v":1, ending in the result — the tasks run --json schema, with cardId: null and sessionId and result added.
--mode auto|plan|manualauto (default) applies edits; plan makes none; manual stops at the first edit and exits 4, since nobody is there to approve it.
--agent <name>, --model <model>The backend, chosen as the terminal chooses it when omitted.
--timeout <secs>Wall-clock bound; defaults to the project's automation.timeoutSec, 0 for none.

Exit codes follow tasks run: 0 answered, 4 the run stopped for a human (a question, or a manual-mode edit), 5 it ended without answering, 6 it could not start (for example a project whose agent is an external CLI), 7 it passed --timeout, 130 Ctrl-C. Output never carries terminal escapes when stdout is not a terminal.

add ​

If the target folder contains a .portbay.json, add reads it and imports the project from that file. Otherwise it registers the project from the supplied flags.

A portbay.yml or .portbay.json arrives with the repo, so the commands it names (startCommand/start_command, pre- and post-start hooks, auto-start, and env such as NODE_OPTIONS or DYLD_* that changes what those run) are printed rather than applied. Read them, then re-run with --trust-commands to apply them. A hostname outside your PortBay domain suffix, a document root outside the project, or a hostname or port another project already uses is refused either way.

bash
portbay add <PATH> \
  --id <id> \
  --name <name> \
  --hostname <hostname> \
  --kind next|vite|php|static|node|flutter|xcode|android|custom \
  --port <port> \
  --proxy <url> [--preserve-host] \
  --start-command <command> \
  --document-root <relative-path> \
  --php-version <version> \
  --web-server caddy|nginx|apache \
  --https true|false \
  --auto-start

--kind defaults to custom. --https defaults to true.

--web-server picks the server behind the Caddy edge and defaults to caddy. A PHP site with no --start-command, or a static site, is served from its document root by the server you pick; any other project gets Nginx or Apache in front of its dev server or --proxy upstream. It is ignored for native app kinds (xcode, android, flutter, expo). The choice is checked with the server itself before anything is saved; a server that isn't installed, or a config it rejects, refuses the add. See Choosing a web server.

--proxy sends the hostname to an upstream instead of localhost:<port>: an IP on a VM or bridge network, an internal name, or a remote URL, such as http://192.168.64.2:8080, http://api.internal:3000 or https://example.com. The upstream receives its own hostname as Host unless you pass --preserve-host, which sends the project's hostname instead. An https:// upstream can't be combined with --web-server nginx|apache: Caddy reaches it directly and verifies its certificate.

runtime pin ​

bash
portbay runtime pin <id> node@22
portbay runtime pin <id> --clear

Pins one project's runtime version. The language must be the one the project's kind runs on, and that version must be installed. The pin rewrites the project's .nvmrc, .node-version or .tool-versions so your terminal agrees, and a running project restarts on the new version at the app's next reconcile. --clear returns the project to its kind's default runtime.

update ​

bash
portbay update <id> --start-command "pnpm dev --turbo"
portbay update <id> --env API_URL=http://localhost:4000 --env DEBUG=1
portbay update <id> --kind next

--env sets the keys it names and leaves the project's other variables alone. --start-command "" clears the command. A kind not in portbay add --help's list is refused before anything is written. With the app running, start and restart wait for it to load the edit into Process Compose, so a restart straight after an update runs the new command.

remove ​

bash
portbay remove <id>
portbay remove <id> --keep-artifacts

--keep-artifacts leaves cert files and live Caddy route artifacts in place when reachable.

logs ​

bash
portbay logs <id> --limit 200 --offset 0

--limit defaults to 200. --offset defaults to 0 (newest).

doctor ​

bash
portbay doctor
portbay --json doctor

A flutter doctor-style environment report. Checks are grouped into categories; each category header shows the worst verdict among its rows ([✓] ok · [!] warning · [✗] fatal), and every row carries an inline fix hint.

CategoryChecks
CoreRegistry loads (project count, schema version, domain suffix); Process Compose daemon reachability; /etc/hosts managed entries reconciled against the registry.
Web routing & TLSCaddy and mkcert (bundled sidecars — see note); local certificate count under the certs directory.
PHP runtimesEvery detected PHP install (version, path, source), flagged when php-fpm is missing so it can't serve sites.
Servicesdnsmasq resolver routing for the wildcard suffix; Mailpit (bundled sidecar); available database engines (MySQL, MariaDB, Postgres, Redis, Mongo, Memcached).
Account & sharingSigned-in account, tier, and project cap; active tunnel count.

PortBay-bundled sidecars (Caddy, mkcert, Mailpit, cloudflared) are never resolved from $PATH on macOS — they ship with the app, so a foreign install is never mistaken for PortBay's own. On Linux, dnsmasq is intentionally distro-managed: PortBay's sidecar wrapper invokes the system package. Live sidecar state isn't observable from outside the daemon, so these checks report as informational rather than a guessed process state, mirroring portbay sidecar status.

doctor exits 0 even when warnings are present; it returns a non-zero code only when a check is fatal (e.g. the registry fails to load). With --json it prints an array of categories, each with a verdict and a checks array of { check, verdict, detail } objects.

hosts ​

bash
portbay hosts list
portbay hosts add <hostname> --ip 127.0.0.1
portbay hosts remove <hostname>
portbay hosts clear
portbay hosts reconcile

hosts add defaults --ip to 127.0.0.1. All write operations (add, remove, clear, reconcile) try the bundled portbay-hosts-helper sidecar first; they fall back to direct /etc/hosts writes, which require elevated privileges.

completions ​

bash
portbay completions bash
portbay completions zsh
portbay completions fish
portbay completions powershell

login ​

bash
portbay login               # GitHub OAuth (opens browser)
portbay login --email <addr>  # email magic link
portbay login --device        # short code, approved from any other device

Drives the GitHub OAuth or email magic-link flow from the terminal, then stores the session in the OS keychain shared with the GUI. Polls for up to 5 minutes. Prints the signed-in username and tier on success. If the browser asks you to confirm a code, check it matches the one the terminal printed.

--device is for a machine with no browser (SSH, a container): it prints a code such as ABCD-EFGH and a URL. Open the URL on any device, enter the code, and the terminal finishes signing in. The code expires after 10 minutes. Under --json the code is written to stderr as one JSON line (userCode, verificationUri) and stdout carries only the final account document.

Non-interactive auth (CI) ​

bash
export PORTBAY_TOKEN=pbt_…
portbay license --json

PORTBAY_TOKEN authenticates every PortBay Cloud call without a stored session and takes precedence over one. It is never written to disk, never refreshed, and is removed from the environment before PortBay starts any child process. A value that does not start with pbt_ is refused (exit 2). Under the token, license and resync fetch the entitlement live rather than reading the cache.

token ​

bash
portbay token create ci-deploy --expires-in-days 90
portbay token list
portbay token revoke <id>

Creates, lists and revokes account tokens. create prints the token once; store it as the PORTBAY_TOKEN secret in your CI. --expires-in-days takes 1-365. These commands need your own sign-in (portbay login): run under PORTBAY_TOKEN, they exit 2 with a message saying so.

license ​

bash
portbay license

Prints the cached effective entitlement: account login, tier (anonymous / free / pro), project cap, sync, and mail entitlements.

logout ​

bash
portbay logout

Clears the saved session and cached entitlement from the OS keychain.

Exit Codes ​

CodeMeaning
0Success
1Generic failure
2User input error (bad project id, missing argument)
3Daemon unreachable
4Port conflict
5Readiness timeout (reserved)
6Permission denied (hosts write requires elevated privileges)
Was this helpful?
Feedback

PortBay is pre-MVP software. Use the docs as an operating guide, not a stability guarantee.