Skip to main content
On the hosted app you need none of this: the released binary already knows its endpoint, keeps your session in one file, and exits 0 or 1. You come here when you point the CLI somewhere else — a dev deployment, a stack you run yourself — or when you script it, isolate it in CI, or hand it an identity instead of a browser sign-in.

Which Receipt the CLI talks to

Every command that reaches a server resolves two URLs first: the gateway, which serves /connect/*, the MCP endpoint and the ingest endpoint, and the sign-in origin, the web origin that hosts the browser approval page. On the hosted app both are the same origin. Trailing slashes are stripped from either.
The target word is validated before any override is applied. receipt setup --target staging-2 --server-url https://gw.example.com fails with receipt connect target must be prod, dev, local, or an http(s) URL, even though --server-url would otherwise have decided the host. Pass a URL, or one of the three names.
Not every command accepts every flag. setup, login and doctor take --target, --server-url and --auth-url. receipt connect status|<connector>|disconnect take --server-url and --auth-url plus a positional target word or URL. receipt connect list|tools|call take --server-url and --target, and no --auth-url — they never sign in. tools and mcp take --server-url as a gateway override, plus --target to pick which saved session to send. workspace takes --target and no URL override. import and observe take --target to choose which saved session uploads.

The prod chain

First non-empty value wins. For prod only, a value whose host is localhost, 127.0.0.1 or ::1 is discarded — and discarding it skips the rest of the environment chain rather than handing the turn to the next variable, so resolution continues at step 4. The sign-in chain filters local hosts the same way. With nothing at all — which in practice means a source checkout rather than a release build — you get:

The dev chain

Same shape with a DEV prefix — RECEIPT_CONNECT_DEV_GATEWAY_URL, RECEIPT_CONNECT_DEV_SERVER_URL, RECEIPT_CONNECT_DEV_URL, then RECEIPT_CONNECT_DEV_URL and RECEIPT_DEV_APP_URL when --auth-url was not passed — with two differences: local hosts are not filtered, and there is no default. Unconfigured, --target dev fails with receipt connect dev URL is not configured. Set RECEIPT_CONNECT_DEV_GATEWAY_URL or RECEIPT_CONNECT_DEV_URL.

The local chain

Gateway: RECEIPT_CONNECT_LOCAL_SERVER_URL, RECEIPT_CONNECT_GATEWAY_URL, RECEIPT_PROXY_SERVER_URL, else http://127.0.0.1: plus RECEIPT_PORT, PORT, or 8787.
The released binary reads no .env file of any kind. --target local therefore means 127.0.0.1:8787 and 127.0.0.1:3000 unless you export the variables in the shell you run it from. If your stack listens elsewhere, export RECEIPT_CONNECT_LOCAL_SERVER_URL and RECEIPT_CONNECT_LOCAL_AUTH_URL before running receipt doctor --target local.

The sign-in origin

--auth-url always wins. After that: Because the hosted default only ever reaches the sign-in origin through that last resort, the hosted app has to serve both the sign-in page and /connect/* on one origin — and it does. Point the CLI at exactly the origin your browser is signed in to: the approval page redirects to /auth/sign-in whenever the request carries no session for that origin.

How two URLs are compared

Deciding whether a saved session belongs to the host you just resolved is one comparison used everywhere: both sides are parsed, localhost is rewritten to 127.0.0.1, trailing slashes are dropped, and the results compared. Scheme and port stay significant. http://localhost:8787 matches http://127.0.0.1:8787/, but not https://localhost:8787 and not port 8788.

Where the session is stored

Every write goes to a temporary file at mode 0600 and is then renamed into place, so the file is never half-written and never group-readable. What each file contains — including the plaintext bearer token — is on Sign in: setup, login, and logout.
RECEIPT_CLI_SESSION_FILE does two things, not one. It sets the active session path and suppresses the per-target files entirely. That makes it the right variable for isolating a CI run, and the wrong one to point at a path two jobs share.
When a command asks for one of the three named targets, the matching per-target file is read first, and the active file only if its own target normalizes to the same name. Ask for a URL instead and no per-target file exists, so whatever the active file holds is used. A file that will not parse, or that is missing its gateway URL, token or organization id, reads as no session rather than as an error — but a file you cannot read for another reason, a permissions problem for instance, throws and exits 1. Expiry is decided locally: the CLI decodes the token’s payload and compares its expiry with the clock. It never verifies the signature, and a payload it cannot read is treated as live. There is no refresh — after the 12 hours, setup, login and the connect family re-run the browser login, while workspace, tools and mcp send the dead token and fail on the gateway’s 401: unauthorized from the workspace, connection-list and MCP routes, and Receipt could not authenticate this task. Its runtime access token must be renewed before retrying. from /connect/tools and /connect/call.

Environment variables

Behaviour and storage

Endpoint resolution

RECEIPT_CONNECT_PUBLIC_GATEWAY_URL, RECEIPT_CONNECT_WORKER_GATEWAY_URL, RECEIPT_CONNECT_PUBLIC_URL, RECEIPT_PUBLIC_BASE_URL, RECEIPT_CONNECT_PROD_GATEWAY_URL, RECEIPT_CONNECT_PROD_SERVER_URL, RECEIPT_CONNECT_PROD_URL, RECEIPT_CONNECT_PROD_AUTH_URL, RECEIPT_CONNECT_URL, RECEIPT_APP_URL, RECEIPT_CONNECT_DEV_GATEWAY_URL, RECEIPT_CONNECT_DEV_SERVER_URL, RECEIPT_CONNECT_DEV_URL, RECEIPT_CONNECT_DEV_AUTH_URL, RECEIPT_DEV_APP_URL, RECEIPT_CONNECT_LOCAL_SERVER_URL, RECEIPT_CONNECT_LOCAL_AUTH_URL, RECEIPT_AUTH_URL, BETTER_AUTH_URL, VITE_BETTER_AUTH_URL, RECEIPT_CONNECT_GATEWAY_URL, RECEIPT_PROXY_SERVER_URL, RECEIPT_PORT, PORT, WEB_PORT — all as ordered in the chains above.

Identity without a browser

Precedence is not uniform, and this is the trap:
  • receipt connect list|tools|call — the environment wins outright. The saved session is read only when RECEIPT_CONNECT_TOKEN is unset, and then only if the gateway host matches. Setting the token without a gateway variable fails with Receipt Connect gateway is unavailable; run 'receipt setup' or set RECEIPT_CONNECT_TOKEN even though a perfectly good session exists.
  • receipt tools and receipt mcp serve — the saved session wins, and the environment is the fallback. --server-url here replaces the gateway while still sending the session token, with no host check; see Tools and MCP.
  • receipt workspace, and receipt connect status|<connector>|disconnect — no environment fallback at all. They require a saved session.
setup and login also set RECEIPT_CONNECT_GATEWAY_URL, RECEIPT_CONNECT_PUBLIC_GATEWAY_URL, RECEIPT_CONNECT_TOKEN, RECEIPT_CONNECT_ORGANIZATION_ID, RECEIPT_CONNECT_WORKSPACE_ID, RECEIPT_CONNECT_WORKSPACE_NAME and RECEIPT_CONNECT_USER_ID in their own process, and only where you have not set them yourself. Nothing reaches your shell.

Companion and client tools

CODEX_HOME locates the Codex configuration for receipt mcp install|status|remove. RECEIPT_CLAUDE_PROXY_BIN and CLAUDEN_BIN override the Claude observer companion binary, which is otherwise looked for beside the CLI itself and then on PATH.

Not read

No .env file, and no shell configuration of any kind. START_ALL_*, RECEIPT_CLI_LOAD_LOCAL_ENV and RECEIPT_CLI_NO_FORCE_EXIT belong to the developer CLI, not this binary. ZERO_UPSTREAM_DB is read but has no effect: the released binary always uses the local or cloud receipt store, never Postgres.

Running in CI or a sandbox

Give the run an identity and a private config directory, and stop a stray command from opening a browser:
Two gateway variables, because two code paths read different ones: receipt connect list|tools|call reads RECEIPT_CONNECT_GATEWAY_URL, while the prod chain that doctor, setup and login resolve reads RECEIPT_CONNECT_PUBLIC_GATEWAY_URL. With gateway and token both set, receipt connect list|tools|call never touches the config directory at all, and receipt tools falls back to the same variables when no session file exists. Gate the job on receipt doctor --json and its exit code rather than on parsing text.

Exit codes

There is no third code from the program itself. Two you may still see come from outside it:
  • 137, with no output. macOS killed the binary before it ran — a quarantined copy, or, far more rarely, a published build whose own ad-hoc signature is malformed. Reinstall with the published installer (Install the CLI); CLI troubleshooting tells the two apart and gives the xattr fix.
  • 127 from the developer CLI, with receipt CLI requires Bun. Set RECEIPT_BUN_BIN or install Bun on PATH. — that is the in-repo shell wrapper, not the released binary.
Results and progress lines both go to standard output — that includes lines like Starting Receipt Connect sign-in at <authUrl>..., Step 1 of 2: sign in to Receipt and Saved Receipt session was rejected by the gateway; signing in again., so parse the --json forms, never the text. Standard error carries only the message a failing command ends with, plus claude observer failed to start: <message> when observe cannot spawn the companion binary.

Output shapes

--json means three different things depending on where it appears: The envelopes:
On receipt tools, ok: true means the gateway answered — not that the provider succeeded. A provider-side failure comes back inside the result as isError content.
tools takes --output or --output-file, writes the envelope atomically at mode 0600, and prints {ok: true, outputFile, bytes} instead. mcp config takes the same two flags and the same atomic write, but prints receipt mcp config: wrote <path> — or, with --json, its usual envelope carrying the path in output. import and observe take --output-file only, write it plainly at your umask, and print the same {ok: true, outputFile, bytes} pointer. doctor, setup, login, workspace and the connect commands have no output-file flag. Next step: look up an error string and its fix.