The CLI is the product. There is no console you have to open, no wizard that owns a step, and nothing you can do in a dashboard that you cannot do from a terminal — which is also why an agent can drive the whole kit without a single screenshot.
This page is what every command shares. What one command does is on its own page.
The shape
pithy <command> [subcommand] [args] [--flags]Verb first, noun second, and the binary is always pithy:
pithy init
pithy add auth
pithy worker add admin-api
pithy migrate --env staging
pithy deploy --env prodFlags every command honors
| Flag | Meaning |
|---|---|
--json | One line, one object, on stdout. Also suppresses every prompt |
--help, -h | Help for any command |
--version, -v | The installed version, from anywhere before a -- separator |
Long flags are kebab-case, short flags are a single letter, booleans default to false, and a value passes with either = or a space — --env=prod and --env prod are the same thing.
--version works after a subcommand as well as before one. That is deliberate: citty, the framework underneath, answers its own version builtin only when it is the sole argument, so pithy add --version would have run add instead. The rule lives in one place and applies everywhere.
--env, and why it is short
--env takes dev, staging, or prod. It defaults to dev, because every command is safe there. It is validated at the flag, before anything loads.
It is not production. The environment sits verbatim in the middle of every Cloudflare name the project composes — <project>-<env>-<thing> — so each of its characters costs one character of project name, one for one. --env production is answered with an error naming prod.
A custom environment is allowed: live, eu-prod. It is held to the same character set and to a hard maximum of seven characters, the length of staging. Every project-name budget is derived against that seven, and a provisioned project cannot be renamed, so a longer environment is refused rather than quietly shrinking a cap other projects were already accepted under.
Which environments a project has is declared once, as environments in the root pithy.config.ts — ["staging", "prod"] unless it says otherwise. A command that deploys or provisions refuses an --env the project does not declare, and names the ones it does. dev is never declared, because it is local and always there.
--worker, and when you need it
Every Worker lives in apps/<name>/ and owns its own config. There is no root Worker.
- Commands that wire one Worker take
--worker <name>:add,remove. In a single-Worker project the flag is optional. In a project with several, the CLI prompts at a terminal and fails with an actionable error under--jsonrather than guessing — wiring a capability into the wrong Worker puts its bindings and its Durable Object class migrations on the wrong script. - Commands that operate on the project fan out over every Worker and accept
--workerto narrow:migrate,seed,upgrade,doctor,env.
Two Workers share a resource by declaring the same binding name. Two Workers that both declare DB are backed by one D1; a Worker that wants its own declares COLLAB_DB.
One command refuses --json
pithy remove is destructive, so it is manual and interactive only. Passing --json fast-fails with a clear error before anything changes, and its --drop confirmations are typed at a real terminal. There is no headless path, on purpose.
Automated teardown of an ephemeral environment is a different command — the pithy feature lifecycle — rather than a flag on this one.
The p. shortcut
Two characters, ending in the brand mark:
pithy aliasp. init, p. add auth, p. deploy. It writes a delimited block into your shell’s rc file, detected from $SHELL and the platform, and it is idempotent — a second run reports Already pithy. and changes nothing. --status reports it, --remove deletes exactly the block that was added.
pithy init offers it once, at the end, and only to a human at a terminal.
What output looks like
Work in progress carries a status arrow. Completion is one word. Errors lead with the problem and follow with the action.
$ pithy add auth
▸ Installing @pithy-sh/auth...
▸ Updating wrangler.jsonc...
Done.There is no emoji, no celebration, and no successfully completed in 2.3s. A genuinely long command appends the timing minimally — Done. (3.2s) — and nothing else does.
An error is two lines:
Couldn't connect to Cloudflare API.
Check your CLOUDFLARE_API_TOKEN and try again.No stack traces reach you; a stack trace means a CLI bug rather than a project problem, and those keep theirs. Multi-row output is whitespace-aligned columns with no borders — no ASCII art, no Unicode box drawing. The whitespace is the layout.
Color is decided once, for the whole program, and respects NO_COLOR, a non-TTY stdout, and CI. Piping pithy --help into something gets you plain text, including from the parts of the help that citty renders rather than Pithy.
Prompts, and what they can answer for you
A prompt shows its default in brackets — [Y/n] means default yes, [y/N] means default no.
A prompt that can be answered from your account, is. pithy init and pithy worker add ask where a Worker will answer, per environment, and offer the account’s real Cloudflare zones rather than a free-text field. A typo fails at init with a list of what exists, instead of at deploy with a Cloudflare error to decode. Where the account cannot be reached — no credentials, a token without Zone:Read, an offline laptop — the prompt says so in one line and falls back to free text. Scaffolding has never required the network.
Every one of those is skippable, and a non-interactive run asks nothing at all.
The update notifier
Pithy checks for a new CLI version in the background and never updates anything by itself.
- At most once every 24 hours, cached in the config directory’s
state.json. - Off-thread, after the command’s real work has finished. It never blocks anything.
- Written to stderr, after
Done., and only when stderr is a TTY — so it never pollutes a pipe or a CI log. - Only for a minor or major release. A patch is suppressed unless the release is flagged as a security fix, because patch noise trains people to ignore the surface entirely.
- A failed check silently keeps the cached value. An update check must never break the CLI.
Turn it off with PITHY_NO_UPDATE_NOTIFIER=1, or persistently with pithy doctor --disable-notifier. pithy doctor --enable-notifier puts it back.
Every command
| Command | What it does |
|---|---|
pithy init | Scaffold a new project and its first Worker |
pithy add | Install a capability into one Worker |
pithy remove | Unwire a capability. Manual only |
pithy dev | Run the whole local environment under one supervisor |
pithy migrate | Run each Worker’s migration registry against an environment |
pithy seed | Load fixtures from your own Zod schemas |
pithy provision | Create an environment’s Cloudflare resources and migrate |
pithy deploy | Ship every Worker, then prove which version answered |
pithy env | Report environments, bindings, ids and dashboard links |
pithy worker | Add, list, remove, rename and sync the Workers under apps/ |
pithy ui | Scaffold a front end into a Worker and wire it end to end |
pithy secrets | Create, rotate, edit and provision the secret registry |
pithy token | Mint, list and revoke scoped Cloudflare API tokens |
pithy doctor | Check the toolchain, the project, and for a new version |
pithy upgrade | Reconcile package-served capabilities with their manifests |
pithy alias | Install or remove the p. shortcut |
pithy feature | Feature environment lifecycle: create, sync, destroy |
pithy dashboard | Register, rotate and revoke a management client’s access |
pithy email | Provision mail, send test messages, inspect the job log |
pithy media | Provision and manage the media backends |
pithy storage | Provision and manage the R2 bucket behind storage |
pithy vector | Provision Vectorize indexes and reprocess embeddings |
pithy payments | Provision and inspect the payment rails |
pithy support | Provision and manage the support inbox |
pithy testers | Cohorts, invitations and tester rosters |
pithy turnstile | Provision the humanity check |
Every page carries the same six sections — synopsis, flags, what it does, --json, errors, examples — so a command’s contract is specified in one place and specified completely. A test in the kit holds each page to naming every key its command’s --json payload emits, which is why the reference does not drift from the code.