The quoin CLI
Change flags, run experiments, attach guards and declare metrics from a terminal.
Every quoin command has the same shape: a group, a verb, its key, and --env naming exactly one environment. There is no default environment: a command without --env refuses to run rather than guess. Metrics are the one exception; they belong to the workspace.
Every write shows what it will change and asks before it writes. --yes skips the question but never the printed change; with no terminal to ask in and no --yes, the command refuses and writes nothing. quoin flags off is the one write that never asks: it is the kill switch.
The CLI acts as you. Everything it writes is recorded in your name with cli beside it, in the flag's history and the audit log.
Running it
The CLI is not published to npm yet. Inside the Quoin repository, run it from source with Node 22.18 or later: node packages/cli/src/index.ts --help.
Signing in
quoin login --url <origin> opens <origin>/cli/authorize in your browser. Pick a workspace, create a CLI key, and paste it into the terminal; the page shows the key once. --token - reads a key from standard input instead, for a machine with no browser.
A CLI key is an operator key for you in one workspace. Revoke it under Settings → API keys. The CLI sends it only over https, or plain http to this machine.
quoin login
quoin login --url <origin>Opens <origin>/cli/authorize, where you create a CLI key, and waits for you to paste it. The key is stored for this workspace.
| Options | |
|---|---|
--url <origin> | Where Quoin runs, like https://quoin.example.com |
--token <key|-> | Paste the key instead of opening a browser; - reads it from standard input |
quoin logout
quoin logout [--workspace <slug>] [--revoke]Removes the stored CLI key for a workspace from this machine. With --revoke it first revokes the key in Quoin, then removes it.
| Options | |
|---|---|
--revoke | Revoke the key in Quoin first, then remove it here |
Where the key is kept
For each command the CLI looks for a key in this order: the QUOIN_OPERATOR_KEY environment variable (it needs QUOIN_URL beside it, and the server says which workspace the key belongs to), then the key stored by quoin login, then a hidden prompt for that command only.
On macOS quoin login keeps the key in the Keychain. Elsewhere it keeps it in ~/.config/quoin/credentials.json, readable by you alone; a file is the weaker store, since any program running as you can read it. QUOIN_STORE=file or QUOIN_STORE=keychain picks one, and QUOIN_CONFIG_DIR moves the file.
To remove the key from this machine, run quoin logout (add --workspace <slug> for one that isn't current). That clears only this machine's copy; quoin logout --revoke first revokes the key in Quoin so it stops working anywhere, then removes it. If the revoke is refused, the key stays stored and the command says why. If the macOS Keychain refuses to delete the key (it is locked, or its prompt was denied), the key stays stored and the command exits with a sentence saying so; an entry whose Keychain item is already gone is removed, with a note that nothing could be revoked.
Signed in to more than one workspace? --workspace <slug> picks one; otherwise the last one you signed in to is used.
Every command
| Options | |
|---|---|
--workspace <slug> | A workspace you signed in to (default: the last one) |
--help | Show this help |
flags
List, read, explain and change flags
quoin flags list
quoin flags list --env <env>Every flag and what it serves in that environment
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
quoin flags get
quoin flags get <key> --env <env>One flag: its definition, its rules and the stored value
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
quoin flags history
quoin flags history <key> --env <env>Who changed it, when, why, and what it held before
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--limit <n> | How many revisions (1 to 100, default 20) |
--before <revision> | Older than this revision, like r-0418 |
quoin flags explain
quoin flags explain <key> --env <env> [--user <id>] [--email <address>]What one person gets and why, rule by rule; records nothing
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--user <id> | The user id to explain for |
--email <address> | People by email, comma-separated; @domain matches a whole domain |
--tier <tier> | A plan tier, comma-separated |
--client <client> | A client, comma-separated |
--owner | Workspace owners |
quoin flags explain exports.csv_v2 --email mia@tessellate.io --env productionquoin flags doctor
quoin flags doctor --env <env>Flags past their expiry date and stored values that no longer validate; exits 1 when it lists any
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
quoin flags doctor --env productionquoin flags rollout
quoin flags rollout <key> <percent> --env <env>Serve the value to that share of people; the same people stay in as it rises
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the history (three characters or more) |
--yes | Write without asking; the diff still prints |
--value <json> | The value to serve (JSON; true for a boolean when left out) |
--email <a,b> | People by email, comma-separated; @domain matches a whole domain |
--user <id,…> | People by user id, comma-separated |
--tier <tier,…> | A plan tier, comma-separated |
--client <client,…> | A client, comma-separated |
--owner | Workspace owners |
--signed-up-before <day> | Signed up before this day, YYYY-MM-DD |
--signed-up-after <day> | Signed up after this day, YYYY-MM-DD |
--confirm "resume anyway" | Type "resume anyway" to resume above the coverage a guard paused at |
quoin flags rollout onboarding.checklist_v2 50 --env production
quoin flags rollout exports.csv_v2 25 --tier pro --note "pro first" --env productionquoin flags force
quoin flags force <key> <value> --env <env> --email <a,b>Pin a value for named people, above every other rule
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the history (three characters or more) |
--yes | Write without asking; the diff still prints |
--email <a,b> | People by email, comma-separated; @domain matches a whole domain |
--user <id,…> | People by user id, comma-separated |
--tier <tier,…> | A plan tier, comma-separated |
--client <client,…> | A client, comma-separated |
--owner | Workspace owners |
--signed-up-before <day> | Signed up before this day, YYYY-MM-DD |
--signed-up-after <day> | Signed up after this day, YYYY-MM-DD |
quoin flags force checkout.new_summary --email @tessellate.io true --env productionquoin flags unforce
quoin flags unforce <key> --env <env> --email <a,b> | --rule <id>Take a pin away
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the history (three characters or more) |
--yes | Write without asking; the diff still prints |
--email <a,b> | People by email, comma-separated; @domain matches a whole domain |
--user <id,…> | People by user id, comma-separated |
--tier <tier,…> | A plan tier, comma-separated |
--client <client,…> | A client, comma-separated |
--owner | Workspace owners |
--signed-up-before <day> | Signed up before this day, YYYY-MM-DD |
--signed-up-after <day> | Signed up after this day, YYYY-MM-DD |
--rule <id> | The rule id to remove (quoin flags get shows them) |
quoin flags set
quoin flags set <key> <value|@file> --env <env>Store a flag's JSON as it is
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the history (three characters or more) |
--yes | Write without asking; the diff still prints |
quoin flags clear
quoin flags clear <key> --env <env>Remove the stored rules: back to the default in code
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the history (three characters or more) |
--yes | Write without asking; the diff still prints |
quoin flags on
quoin flags on <key> --env <env>Lift the kill switch; the rules serve again
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the history (three characters or more) |
--yes | Write without asking; the diff still prints |
--confirm "resume anyway" | Type "resume anyway" to resume above the coverage a guard paused at |
quoin flags on exports.csv_v2 --confirm "resume anyway" --env productionquoin flags off
quoin flags off <key> --env <env>The kill switch: serves the default at once, keeps the rules, never asks
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the history (three characters or more) |
quoin flags off exports.csv_v2 --env productionquoin flags revert
quoin flags revert <key> --to <revision> --env <env>Go back to what a revision stored; the revert is itself a new revision
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the history (three characters or more) |
--yes | Write without asking; the diff still prints |
--to <revision> | The revision to go back to, like r-0418 |
--confirm "resume anyway" | Type "resume anyway" to resume above the coverage a guard paused at |
quoin flags revert onboarding.checklist_v2 --to r-0418 --env productionquoin flags promote
quoin flags promote <key> --from <env> --to <env>Copy a flag's rules from one environment to another, showing both halves first
| Options | |
|---|---|
--note <text> | Why, for the history (three characters or more) |
--yes | Write without asking; the diff still prints |
--from <env> | The environment to copy from |
--to <env> | The environment to copy to |
--confirm "resume anyway" | Type "resume anyway" to resume above the coverage a guard paused at |
quoin flags promote exports.csv_v2 --from staging --to productionexperiments
Plan, start, read, stop and conclude experiments
quoin experiments list
quoin experiments list --env <env>Every experiment in that environment, its status and how it reads
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
quoin experiments status
quoin experiments status [key] --env <env>What is running or stopped and how each reads today; with a key, one experiment
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
quoin experiments get
quoin experiments get <key> --env <env>One experiment: its pre-registered design and where it stands
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
quoin experiments report
quoin experiments report <key> --env <env>The report: health checks first, then the verdict and the sequential 95% intervals
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--phase <n> | An earlier phase, by number |
quoin experiments report trial-reminders --env productionquoin experiments new
quoin experiments new <key> --flag <flag> --metric <metric> --env <env>Plan an experiment on a flag and save it as a draft; nothing runs until start
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--flag <flag> | The flag it runs on (boolean flags only) |
--metric <metric> | The primary metric, like activation_rate.7d |
--hypothesis <text> | What will change, for whom, and why you think so |
--guardrail <key[:margin],…> | Guardrail metrics, comma-separated; key:margin sets a margin, like errors.1d:0.5 |
--decision-rule <text> | What result ships it (default: composed from the metric and the guardrails) |
--mde <percent> | The smallest change worth detecting, in percent (default: what the plan can detect) |
--weights <a/b> | The split, control first, like 50/50 |
--coverage <percent> | The share of eligible people in the experiment, in percent (default 100) |
--yes | Write without asking; the diff still prints |
quoin experiments new trial-reminders-v2 --flag billing.trial_reminder_cadence --metric activation_rate.7d --hypothesis "Two reminders instead of three keep more trials without more cancellations" --env productionquoin experiments start
quoin experiments start <key> --env <env>Lock the plan and start; the flag's rules freeze until it stops
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--weeks <n> | How long it runs (default: the weeks the plan chooses) |
--anyway | Start even though the plan warns |
--yes | Write without asking; the diff still prints |
quoin experiments start trial-reminders-v2 --weeks 4 --env productionquoin experiments stop
quoin experiments stop <key> --reason <text> --env <env>Stop counting exposures; the record and the report stay
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--reason <text> | Why it stops, for the record |
--yes | Write without asking; the diff still prints |
quoin experiments stop digest-send-time --reason "sample ratio" --env productionquoin experiments conclude
quoin experiments conclude <key> --ship <variation> | --rollback | --inconclusive --why <text> --env <env>Write the permanent decision record: ship a variation, roll back, or inconclusive
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--ship <variation> | Ship this variation |
--rollback | Roll back to control |
--inconclusive | Record that it settled nothing |
--why <text> | Why, for the decision record |
--confirm-harm <metric> | Ship over a harmed guardrail: type the guardrail's name |
--yes | Write without asking; the diff still prints |
quoin experiments conclude trial-reminders --rollback --why "guardrail harm confirmed, whole interval outside ±0.5pt margin" --env production
quoin experiments conclude instant-search-results --ship treatment --why "clear lift, no guardrail harmed" --env productionquoin experiments discard
quoin experiments discard <key> --env <env>Throw a draft away; anything that ran keeps its record
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--yes | Write without asking; the diff still prints |
guards
Attach guards that pause a rollout when a rate passes its ceiling
quoin guards list
quoin guards list --env <env>Every guard in that environment and its state
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--flag <flag> | Only this flag |
quoin guards attach
quoin guards attach <flag> <name> --event <name> --numerator <event> --denominator <event> --threshold <pct> --baseline <pct> --checks <n> --env <env>Watch a rate on a flag, checked every hour; past its ceiling for enough checks in a row, it pauses the rollout
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--event <name> | The signal: event name for the guard, like error_rate |
--window <1h|1d|7d> | The window the rate is read over: 1h, 1d or 7d |
--numerator <event> | Events counted on top of the rate |
--denominator <event> | Events counted under the rate |
--threshold <percent> | Pause above this rate, in percent, like 1.2 |
--baseline <percent> | The usual rate, in percent, like 0.6 |
--checks <n> | Consecutive hourly checks over the ceiling before it pauses (1 to 24) |
--note <text> | Why, for the audit log (three characters or more) |
--yes | Write without asking; the diff still prints |
quoin guards attach exports.csv_v2 csv-errors --event error_rate --numerator export_failed --denominator export_started --window 1d --threshold 1.2 --baseline 0.6 --checks 2 --note "watch the widening" --env productionquoin guards detach
quoin guards detach <flag> <name> --env <env>Stop watching; a guard that has paused its flag stays until you resume or roll back
| Options | |
|---|---|
--env <env> | The environment, like production — every command names one |
--note <text> | Why, for the audit log (three characters or more) |
--yes | Write without asking; the diff still prints |
metrics
List, read and declare the metrics experiments measure
quoin metrics list
quoin metrics listEvery metric in the workspace; metrics aren't per environment
| Options | |
|---|---|
--env <env> | Accepted and ignored: metrics belong to the workspace, not an environment |
quoin metrics get
quoin metrics get <key>One metric: what it counts, over how long, and which way is better
| Options | |
|---|---|
--env <env> | Accepted and ignored: metrics belong to the workspace, not an environment |
quoin metrics declare
quoin metrics declare <key> --kind <kind> --event <name> --window <days> --better <higher|lower>Declare a metric from an event you send
| Options | |
|---|---|
--env <env> | Accepted and ignored: metrics belong to the workspace, not an environment |
--title <text> | The name people read (default: the key) |
--kind <binomial|count> | binomial (did it happen) or count (how many times) |
--event <name> | The event it counts, like item_saved |
--window <days> | Days after first exposure the metric reads over (1 to 90) |
--better <higher|lower> | Which way is better: higher or lower |
--winsorize <fraction> | For a count, cap outliers at this quantile, like 0.99 |
--anyway | Declare it before the event has arrived; it reads 0 until it does |
--yes | Write without asking; the diff still prints |
quoin metrics declare items_saved.14d --kind count --event item_saved --window 14 --better higherExit codes
| Code | |
|---|---|
| 0 | Done, or nothing needed doing. |
| 1 | Quoin refused the change, the key was not accepted, or flags doctor found something. The reason is printed as Quoin wrote it. |
| 2 | The command itself was wrong: a missing --env, an unknown option, or nothing to ask in. |
Every command is copyable
Wherever the web app shows a command — a flag's page, an experiment's report, Home — it is a chip with the key and the environment already filled in, and the CLI runs it as shown. A test runs every chip the app and these docs print through the CLI's parser.
Next
Database connections
How Quoin connects to your Postgres read replica — the read-only role it needs, what it checks before saving, how it stores the connection string, and why an IP allow-list of Cloudflare's shared ranges is never isolation.
API
tRPC for the web app and the CLI; plain HTTP for anything that needs to run outside a TypeScript process. One error shape either way.