CLI
Every pitch command and flag.
On this page
- How pitch works
- Rehearse, then commit
- What acts at once
- Which project a command acts on
- Flags
- Output
- Commands
- pitch login
- pitch logout
- pitch whoami
- pitch config
- pitch auth header
- pitch init
- pitch validate
- pitch dev
- pitch deploy
- pitch pages list
- pitch pages add
- pitch pages remove
- pitch pages push
- pitch link
- pitch links
- pitch link copy
- pitch open
- pitch community status
- pitch community card
- pitch community list
- pitch community withdraw
- pitch logs
- pitch rollback
- Preview deploys
- pitch --help
- Environment
- What pitch needs
- Exit status
pitch is the command line for Roomi. It writes and checks
pitch.json, rehearses a deploy on your own machine, publishes exactly what
the rehearsal checked, and makes a private link for each viewer. This page
lists every command and every flag it takes.
pitch is published on npm as @pitch-product/cli, and needs Node 24 or
later. Run it with npx @pitch-product/cli <command>, or install it with
npm install -g @pitch-product/cli and run pitch <command>. It talks to
Roomi, https://api.getroomi.com, unless you point it elsewhere
(pitch config). Install the CLI covers
setting it up.
Most people start with the connector, from a chat with Claude,
and come to pitch for a container demo, a build bigger than a chat carries,
work in a repository with Claude Code, or a script. Install the
CLI says when, and the connector says so too.
How pitch works
Rehearse, then commit
pitch deploy on its own is a rehearsal. It checks pitch.json, scans the
project for secrets, builds the demo, runs it and requires it to answer, runs
every step of the story on that running copy, and prints what would go live.
Nothing leaves your machine. pitch deploy --commit does the same and then
publishes exactly the artefact the rehearsal just checked.
pitch validate is the quick half of the rehearsal: it checks pitch.json
and what it names without building or running anything.
Every pitch community command that changes something rehearses the same
way: it reads, runs the pre-flight check (and, for a listing, the listing
checks, writing nothing), and prints what would change.
--commit makes the change.
What acts at once
pitch link, pitch pages add, pitch pages remove and pitch pages push
are not rehearsed. Each is one small change, and each prints what it did and,
where there is one, the id that undoes it. The one change that would lose
something, replacing a page's content with a file of the same title, is
refused unless you pass --replace.
Which project a command acts on
A project is named by its slug, the part of the room's address before the
domain. Without --project, pitch makes the slug from the name in the
pitch.json in the current folder: lower-case, every run of other characters
turned into one hyphen, anything before the first letter dropped, at most 40
characters. "Corner for Bullring" is corner-for-bullring.
--project <slug> names the project instead. pitch deploy, pitch link,
pitch open, every pitch pages command and every pitch community command
take it.
Flags
Every flag is read by one parser before any command runs, so:
- a flag pitch does not know stops it with an error before anything happens;
- a flag that takes a value and is given none stops it the same way;
- a flag a command does not use is ignored.
pitch validate --commitvalidates.
-y is short for --yes, and -h for --help.
Output
What pitch reports goes to standard output; a refusal or a failure goes to
standard error. A rehearsal prints one line per check, starting ok, FAIL
or note:
ok manifest: pitch.json validates (container, 4 apps, a 5-step story)
ok secrets: gitleaks scanned the project and found nothing
ok build: docker build --platform linux/amd64 -t pp-build/corner-for-bullring:latestA note never stops anything. A FAIL stops the command and says why. A
problem in pitch.json names where it is and prints the address of the docs
that explain it.
Errors lists every one.
Commands
pitch login
pitch loginSigns pitch in to the platform as you. It asks the platform for a code, prints an address and the code, opens that address in your browser, and waits while you sign in to the app and confirm the code. If the browser cannot be opened, open the printed address yourself. When you do, the platform issues a token and pitch stores it in the macOS Keychain, one entry per platform address. It never writes the token to a file or prints it.
This is the device sign-in of OAuth (RFC 8628). When the platform gives one, pitch also prints an address with the code already filled in. pitch polls at the interval the platform asks for, slows down when told to, and gives up when the code expires.
Takes no flags.
Open https://app.getroomi.com/device and enter the code <code>
Opened it in your browser.
Waiting for you to confirm…
Signed in. The token is in the macOS Keychain.Exit status: 0 once signed in. 1 when sign-in could not start, was denied in the browser, failed, or the code expired before it was confirmed.
pitch logout
pitch logoutEnds the session on the platform, so the token stops working, and removes it from the Keychain. If the platform does not confirm the sign-out, pitch says so and removes the token from this machine anyway.
Takes no flags. Prints Signed out., or Not signed in. when there was no
token. Exit status: 0.
With PP_TOKEN set, pitch logout ends that token's session on the platform,
then stops with an error, because there is no Keychain entry to remove, and
exits 1.
pitch whoami
pitch whoamiSays which platform pitch is talking to and where that came from, then asks it who the stored token belongs to, and prints their name and email:
API https://api.getroomi.com (the default)
Priya Shah <priya@example.com> on https://api.getroomi.comTakes no flags. Exit status: 0 when the token is valid; 1 when there is no
token, or the platform no longer accepts it (run pitch login again).
pitch config
pitch config
pitch config get api
pitch config set api <url>
pitch config unset apiWhich platform pitch talks to, kept for you in ~/.config/pitch/config.json
(under XDG_CONFIG_HOME when that is set), readable by you alone. pitch talks
to https://api.getroomi.com unless this says otherwise, or PP_API
does, which wins. You need it only to work on Roomi itself, against a
platform running on your own machine. set api takes an origin only, such as
http://127.0.0.1:8200 for a platform on your own machine; plain http is
refused anywhere else. Your sign-in is kept per platform, so after switching,
pitch whoami says whether you are signed in there. A settings file pitch
cannot read stops every command, rather than being ignored.
Acts at once and says what it did. Exit status: 0; 1 for an address pitch refuses.
pitch auth header
pitch auth header [--print]Prints the stored token as the one JSON object an HTTP client reads as a header, and nothing else on standard output:
{"Authorization":"Bearer <token>"}The Claude Code plugin reads it this way to call the connector as you, and a script can use it to call the HTTP API.
| Flag | What it does |
|---|---|
--print |
Prints the header even when standard output is a terminal. Without it, pitch refuses to print a token to a screen a person is looking at, and prints it only into a pipe or a file. |
Exit status: 0 when the header was printed. 1 when standard output is a
terminal and --print was not given, or when pitch is not signed in.
pitch init
pitch init [--print] [--force] [--yes] [--kind container|static] [--output <dir>]
[--health <path>] [--device <app>=<device>]…
pitch init --update [--print]Reads the repository in the current folder and writes a first pitch.json,
and beside it pitch.README.md, which says what pitch found and shows the
shape of a story on your own apps, and PITCH.md, the guide for a coding
agent working in the repository. It adds one line pointing at PITCH.md to
the end of AGENTS.md and CLAUDE.md (making an AGENTS.md if there is
neither), unless the file already mentions it. It never changes your code
(Build with your AI agent).
It reads the name from package.json; whether the build is a server (a
Dockerfile at the root) or a folder of files, and for a folder, where the
build writes it (a Next export's distDir, read from next.config with the
variables the build script sets: Static demos);
the apps and the path each is
served under; the device each is for, from its name or its layout; a health
route, a reset route and demo routes it can call; and the Markdown and HTML
files in docs/ and documentation/, which it lists as pages. It does not
write a story: that is how you tell the demo, and it cannot know it.
What it cannot tell, it asks, after printing any note that explains why.
With nobody at the terminal to ask (Claude Code, a script), it stops, names
the flag that answers each question, and writes nothing. A repository whose apps each run on their own port is not one origin
yet; pitch init then prints the change that would make it one, and writes
nothing.
| Flag | What it does |
|---|---|
--print |
Prints pitch.json, pitch.README.md and PITCH.md, and the line it would add to AGENTS.md or CLAUDE.md, instead of writing them. Writes nothing, and runs even when the files exist. |
--force |
Replaces pitch.json, pitch.README.md and PITCH.md if they exist. Without it, pitch stops rather than overwrite any of them. |
--update |
Rewrites PITCH.md from the docs as they are now, and touches nothing else. Says so and writes nothing when it is already current. |
--yes, -y |
Takes the suggested answer to every question that has one (the output folder dist, the health path /healthz). A question with no suggested answer is still asked, or named: a Next export's folder, when pitch cannot read its distDir, is one. |
--kind <kind> |
container (a server built from the Dockerfile) or static (a built folder of files). Anything else is refused. |
--output <dir> |
The folder a static build writes, relative to the project. |
--health <path> |
The path a container build answers on once it is up. |
--device <app>=<device> |
The device the room frames an app in: phone, tablet, laptop, screen or none. Give it once per app. An app id pitch did not find, or a device that is not one of these, is refused. A flag overrides what pitch read. |
pitch init --print
pitch init --kind static --output build --yes
pitch init --device client=phone --device hq=laptop --forceExit status: 0 when the files were written (or printed). 1 when either file
exists without --force, the repository is not one origin, a flag was
malformed, a question went unanswered, or the draft did not validate.
The fields it writes are in pitch.json.
pitch validate
pitch validateChecks everything that can be known without building: pitch.json against
the schema, fields pitch does not know, the story, the pages on disk, the
static output folder, and the container Dockerfile as far as it can be read.
It builds, runs and sends nothing.
Each problem names where it is (story[1].app) and the docs page that
explains it. A problem is something pitch deploy would refuse; a note is
advice and stops nothing.
pitch.json does not validate: 1 problem
story[1].app: step "moment" happens in app "coach", which apps does not declare
https://getroomi.com/docs/pitch-json#field-story-appTakes no flags. Exit status: 0 when nothing would stop a deploy, notes or not; 1 when there is at least one problem. Every check is in Errors.
pitch dev
pitch dev [--port <n>] [--target <url>] [--static] [--no-open]Serves the real pitch room on your machine, around the demo as you build it:
the same page, script and stylesheet the platform serves a viewer, with your
own dev server on the stage. Run it beside npm run dev (or next dev,
vite, your server), and it opens the room in your browser.
Preview locally walks through it.
- The room is at
http://<slug>.localhost:8300and the demo inside it athttp://<slug>--demo.localhost:8300: two addresses, as a room and its demo are. Both are this machine only; pitch dev listens on 127.0.0.1. - Everything in the room works: the tour, with each step's page, frame and ring; Read more; the next steps; comment mode, with its screenshots. None of it is recorded or sent. The room says Local preview — not saved, and comments are listed in pitch dev's panel on the room, and printed here.
- A container build's calls (a step's
run, the scenarios, the reset) are made on your dev server, as the room makes them on a viewer's copy. pitch.json, its pages andpitch.brand.jsonare watched. When they change, the room redraws. Your dev server's own hot reload shows through the stage as it always does.- What is wrong is counted on the room and listed in its panel, and printed
here, in
pitch validate's words and the rehearsal's: a problem inpitch.json, a page file missing, a step's page your dev server answers with a 404, a call the demo refused.
| Flag | What it does |
|---|---|
--port <n> |
The port the room and the demo are served on. The default is 8300. |
--target <url> |
Your dev server, such as http://127.0.0.1:3000. Without it, pitch dev reads the port from package.json's dev, start, serve and preview scripts (the port a script names, or its tool's default: 3000 for next dev, 5173 for Vite), then tries the common dev ports. Only an address on this machine is accepted. |
--static |
Serves a static build's output folder (output) instead of a dev server, as the platform serves a static build: each path, or the index.html inside it. Build it first. |
--no-open |
Leaves the browser alone. --open, opening it, is the default. |
ok pitch dev: no problems
room http://columbus.localhost:8300
demo your dev server at http://127.0.0.1:3000 (`dev` in package.json runs next dev), framed at http://columbus--demo.localhost:8300
Local preview: nothing is recorded or sent, and comments stay on this machine.
pitch.json, its pages and pitch.brand.json are watched; the room redraws when they change.
Opened it in your browser.
Ctrl-C stops it.pitch dev needs a pitch.json that validates to start; once it is running, a
pitch.json that stops validating leaves the last room on screen and says
why. It never signs in and never talks to the platform. Exit status: 0 when
stopped with Ctrl-C; 1 when it could not start (no pitch.json, the port in
use, --static on a container build, a --target that is not on this
machine).
pitch deploy
pitch deploy [--commit] [--project <slug>] [--note <text>]Rehearses the deploy and, with --commit, publishes it. In order:
- Validates
pitch.jsonaspitch validatedoes. A problem stops here. - Checks you are signed in, with
--commitonly, before anything slow. - Scans the project for secrets with gitleaks. Your own
.gitleaksignoreis not read, so a finding cannot be waved through from inside the build. - Builds.
- A static build runs the
buildscript inpackage.json(with pnpm, yarn, bun or npm, by the lockfile), requiresindex.htmlin the output folder, scans the output for secrets, and packs it. - A container build needs Docker running. It builds your
Dockerfileforlinux/amd64, reads how the image starts from itsCMDandENV(unlesspitch.jsonsetsrun), copies/appout of the image as the bundle, and scans the bundle for secrets.
- A static build runs the
- Runs it, for a container build: starts the platform's own runner on
loopback, hands it the bundle as the platform will, and requires the
healthpath to answer. pitch's own calls during the rehearsal go to that copy on loopback, and nowhere else. - Runs the story: every step's calls, in order, on that copy, then the page the step opens. A step whose call the demo refuses, or whose page does not answer, fails the deploy. On a static build, every page a step opens must be a file in what was built.
- Prints what would go live: the project, the artefact's size, the apps, how it starts, the controls, the story and the pages.
Without --commit it stops there, and nothing has left the machine. With
--commit it goes on:
- Creates the project if its slug is new.
- Uploads the artefact, registers the build, uploads every page
pitch.jsonlists, and makes the build live. A page that will not upload leaves the previous build live. - Prints the room's address where it opens (a listed room), or else the
next steps the plan allows: on Free, which has no viewer links,
pitch opento see it and listing it from the app to show anyone; on a paid plan,pitch link <name>for each viewer's own link.
| Flag | What it does |
|---|---|
--commit |
Publishes what the rehearsal checked. Without it, nothing is sent. |
--project <slug> |
Deploys to this project instead of the one named by pitch.json. Needed when the name makes no usable slug (fewer than three characters). |
--note <text> |
A note kept with the build, up to 200 characters. |
pitch deploy
pitch deploy --commit --note "New onboarding flow"
pitch deploy --commit --project corner-demo What would go live: project "corner-for-bullring" at https://api.getroomi.com
Corner for Bullring — container, health /healthz
artefact 41.2 MB (43198464 bytes)
apps:
client /app phone Bullring (opens first)
hq /hq/ laptop Corner HQ
starts with: node server.js
story:
1 arrive in client "Where it starts"
Rehearsal only: nothing has left this machine. `pitch deploy --commit` publishes exactly this.Exit status: 0 when the rehearsal passed (and, with --commit, the build is
live). 1 at the first thing that stops it. Every stop is in
Errors.
pitch pages list
pitch pages list [--project <slug>]Lists the room's pages in the order the room shows them, with each one's
position, id, kind (markdown or html) and title. Exit status: 0; 1 when
pitch is not signed in or the project does not exist.
pitch pages add
pitch pages add <file.md|file.html> [--title <title>] [--replace] [--project <slug>]Adds one page to the room. Viewers see it at once. The file is a Markdown
(.md) or HTML (.html) file of up to 2,000,000 characters. Its title is
--title, else its first # heading (Markdown) or its <title> (HTML), else
its file name; a title is 1 to 80 characters.
If the room already has a page with that title, pitch changes nothing and
says so, unless you pass --replace.
| Flag | What it does |
|---|---|
--title <title> |
The page's title, instead of the one in the file. |
--replace |
Replaces the content of the page that already has this title. |
--project <slug> |
The project to add it to. |
Added "Why Corner" (markdown) to corner-for-bullring as page 7c5e0a9b-41d2-4f86-b3e7-2a9d6c1f8e04.
Viewers of https://corner-for-bullring.getroomi.app see it now. `pitch pages remove 7c5e0a9b-41d2-4f86-b3e7-2a9d6c1f8e04` takes it out.Exit status: 0 when the page was added or replaced; 1 otherwise.
pitch pages remove
pitch pages remove <id> [--project <slug>]Takes a page out of the room by its id, which pitch pages list shows. If
pitch.json here lists the same page, pitch says so: the next deploy uploads
it again unless you take it out of pages. Exit status: 0 when the page was
removed; 1 when there is no page with that id.
pitch pages push
pitch pages push [--project <slug>]Uploads every page pitch.json lists, as pitch deploy --commit does,
without deploying a build. A page with the same title as one the room has
replaces it. Exit status: 0; 1 when pitch.json does not validate or lists no
pages.
pitch pages with no subcommand prints the four commands and exits 0; an
unknown subcommand prints them and exits 1. More on pages in
Pages.
pitch link
pitch link <name> [--email <address>] [--expires <days>] [--project <slug>]Makes a private link to the room for one viewer, named by <name> (every
word after link, so pitch link Priya Shah names Priya Shah). The link is
printed for you to send; if it is lost, pitch link copy
shows it again. (pitch link copy … is that command, never a link for
someone called Copy.)
| Flag | What it does |
|---|---|
--email <address> |
The viewer's email, kept with the link. Nothing is sent to it. |
--expires <days> |
The link stops opening the room after this many days: a whole number from 1 to 365. Without it, the link does not expire. |
--project <slug> |
The project the link opens. |
A link for Priya Shah, until 2026-10-29:
https://corner-for-bullring.getroomi.app/l/…
Send it to them. If it is lost, `pitch link copy "Priya Shah"` shows it again.Exit status: 0 when the link was made. 1 with no name, a malformed
--expires, no project, or a refusal from the platform. On the Free plan the
platform refuses every private link: see Plans and limits.
pitch links
pitch links [--show] [--project <slug>]Lists the project's viewer links, one line each: the viewer, their email,
the link's stage (or revoked, expired), when it expires, and its id. No
address is printed unless you ask.
| Flag | What it does |
|---|---|
--show |
Prints each live link's address under it. Each one is asked for separately and recorded in the project's history. A link made before 1 October 2026 says to reissue it in the app instead. |
--project <slug> |
The project whose links to list. |
Exit status: 0 when the links were listed. 1 with no project, or a refusal from the platform.
pitch link copy
pitch link copy <name or id> [--project <slug>]One live link's address again, when the viewer has lost it: by the viewer's
name (any case) or the link's id from pitch links. It is printed, and at a
terminal on a Mac copied to the clipboard too; piped, it is only printed.
Recorded in the project's history.
Priya Shah's link:
https://corner-for-bullring.getroomi.app/l/…
Recorded in the project's history.Exit status: 0 when the address was shown. 1 with no name, no live link of that name, two live links of that name (it lists their ids: give one), or a link made before 1 October 2026, which cannot be shown again and is reissued in the app.
pitch open
pitch open [--project <slug>]Opens the room with macOS's open. A project listed on the community opens
at its own address, which pitch prints. A private room's address admits
nobody, so pitch opens it as a viewer sees it, on a one-time pass (the app's
Preview as viewer), and prints the pass only when open could not use it.
Exit status: 0 when it opened; 1 when open failed, nothing is live yet,
pitch is not signed in, or the project does not exist.
pitch community status
pitch community status [--project <slug>]The project's Community card and where its listing stands: an unlisted
draft, listed (with its public address), blocked by a check (with what it
found), taken off by Roomi (with its comment, as the app shows it), or
waiting for review. Also what the pre-flight check found when listing was
last asked for. Changes nothing. To ask for a review of a block, or of a
listing taken off, use Ask for a review in the app, or appeal_listing
from Claude: pitch has no command for it.
Listing on the Community walks through it.
pitch community card
pitch community card [--title <title>] [--line <line>] [--cover <image>] [--commit] [--project <slug>]The card strangers see on the Community. --title is up to 60 characters
and --line up to 140; a first card needs both, and a saved one keeps
whichever you leave out. --cover is a PNG, JPEG or WebP image on this
machine, at most 1 MB, its type read from its bytes: a file that is not one,
or is bigger, stops pitch before the platform hears anything.
Without --commit it rehearses: it prints the card as it would be, and runs
the pre-flight check on the project
with those words, which names anything that might identify a real business.
With --commit it saves the words, then the cover, through the same routes
as the app. A listed card is checked again as it now reads: it stays listed
if the checks pass, and is blocked, off the Community, if one fails; the
rehearsal says so first. A new cover shows on the Community once its own
check has passed.
pitch community list
pitch community list [--commit] [--project <slug>]Lists the project on the Community, once its checks pass. Without
--commit it runs the checks without writing anything and says what
committing would do: "Would be listed now", "Would be blocked" with what a
check found, or "Would wait for review" (after Roomi took the project
off before). Then it prints what listing means (public at one address, five
people at once, on Free total views as the only analytics, checked again on
every change) and what the pre-flight check finds. With --commit the checks
run for real: it prints "Listed: the checks passed, and it is public at …",
"Not listed: a check blocked it.", or that it was sent for review. It needs a
saved card and a live build, and says which is missing.
pitch community withdraw
pitch community withdraw [--commit] [--project <slug>]Takes a listed project off the Community at once, or a waiting one out of
review. Without --commit it says what would stop; with it, the card is kept
as an unlisted draft. A project that is neither has nothing to withdraw.
pitch logs
pitch logsNot built yet. It prints that the API has no endpoint for a live build's logs yet, and exits 1.
pitch rollback
pitch rollbackNot built yet. It prints what it will do, list the builds and make the previous one live, and exits 1. Until then, make an earlier build live from the app.
Preview deploys
pitch --help
pitch
pitch --help
pitch deploy --helpPrints a summary of every command. --help (or -h) after any command prints
the same summary and does nothing else: pitch deploy --help never deploys.
Exit status: 0. An unknown command, pitch help included, prints the same
summary and exits 1.
Environment
pitch reads these variables. None of them is required.
| Variable | What it does |
|---|---|
PP_API |
The platform pitch talks to, ahead of pitch config. The default is https://api.getroomi.com. A trailing slash is ignored. The Keychain keeps one token per platform address, so a local token and another platform's never stand in for each other. |
PP_CONFIG |
A settings file to read and write instead of ~/.config/pitch/config.json. |
PP_TOKEN |
A token to use instead of the Keychain, for scripts. pitch reads it and never stores it. While it is set, pitch login and pitch logout are refused. |
PP_KEYCHAIN |
A Keychain file to keep the token in, instead of your default Keychain. |
PP_GITLEAKS |
The gitleaks program the secret scan runs. The default is gitleaks on your PATH. |
PP_HEALTH_TIMEOUT_MS |
How long a container rehearsal waits for the platform's runner to open its control port, in milliseconds. The default is 60000. |
What pitch needs
- Node 24 or later.
- gitleaks for
pitch deploy, on yourPATHor atPP_GITLEAKS. A scan that could not run is not a clean scan, so the deploy stops. - Docker, running, for a container build.
- macOS for
pitch login, which keeps the token in the Keychain, and forpitch open. Elsewhere, pass a token inPP_TOKEN.
Exit status
Every command exits 0 when it did what it says and 1 when it did not; a rehearsal that passed is a 0. A flag pitch does not know, or one missing its value, stops it with a non-zero status before the command runs. A script can rely on these:
pitch validate && pitch deploy && pitch deploy --commit