CLI

Every pitch command and flag.

On this page

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 --commit validates.

-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:latest

A 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 login

Signs 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 logout

Ends 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 whoami

Says 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.com

Takes 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 api

Which 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 --force

Exit 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 validate

Checks 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-app

Takes 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:8300 and the demo inside it at http://<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 and pitch.brand.json are 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 in pitch.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:

  1. Validates pitch.json as pitch validate does. A problem stops here.
  2. Checks you are signed in, with --commit only, before anything slow.
  3. Scans the project for secrets with gitleaks. Your own .gitleaksignore is not read, so a finding cannot be waved through from inside the build.
  4. Builds.
    • A static build runs the build script in package.json (with pnpm, yarn, bun or npm, by the lockfile), requires index.html in the output folder, scans the output for secrets, and packs it.
    • A container build needs Docker running. It builds your Dockerfile for linux/amd64, reads how the image starts from its CMD and ENV (unless pitch.json sets run), copies /app out of the image as the bundle, and scans the bundle for secrets.
  5. 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 health path to answer. pitch's own calls during the rehearsal go to that copy on loopback, and nowhere else.
  6. 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.
  7. 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:

  1. Creates the project if its slug is new.
  2. Uploads the artefact, registers the build, uploads every page pitch.json lists, and makes the build live. A page that will not upload leaves the previous build live.
  3. 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 open to 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 <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 [--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 <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 logs

Not built yet. It prints that the API has no endpoint for a live build's logs yet, and exits 1.

pitch rollback

pitch rollback

Not 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 --help

Prints 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 your PATH or at PP_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 for pitch open. Elsewhere, pass a token in PP_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