receipt. It reads receipt streams, drives the job queue, runs deterministic simulations and evaluations, and controls Factory end to end — the whole developer surface the released binary leaves out. It runs the TypeScript source directly, so an edit to a CLI file takes effect on the next invocation, and it shares its saved session with the released binary: sign in with one and you are signed in for the other.
This page covers the program itself — how it starts, what it reads, and how it parses what you type. The commands are on the pages beside it.
Receipts and jobs
Trace, replay, inspect and fork streams; enqueue, steer and abort jobs.
Factory concepts
Objectives, tasks, candidates, checks and the promotion gate.
Factory command reference
Every
receipt factory subcommand, flag and envelope.Testing and simulation
The check pyramid, deterministic simulation, evals and stack validation.
Which receipt am I running?
Two programs share the name. The one you install on your own machine is the released binary, documented in The receipt CLI. The one on this page runs only from a git checkout, launched by the shell wrapper .receipt/bin/receipt, and needs Bun — plus Postgres for most of its commands. Its entry file is packages/receipt-app/src/cli.ts. Local development covers getting that checkout and its database running in the first place.
The surfaces overlap more than their sizes suggest: setup, login, logout, connect, workspace, tools, mcp, import and observe exist in both, and both answer to doctor — though doctor is a different command in each. Two commands tell the programs apart:
The two
doctor commands answer different questions. The released binary’s receipt doctor resolves your endpoint and reports the saved session, over the network, without signing in. This one never touches the gateway: it probes local binaries, the git repository, the Factory config and your cloud logins. Neither one checks a model key.How the wrapper starts it
.receipt/bin/receipt is a #!/bin/sh script, not TypeScript. In order, it:
- Resolves symlinks to find its own real path, sets the root directory to two levels above itself, and
cds there. This is why you can symlink the wrapper onto yourPATHfrom anywhere and still have it run against the checkout it belongs to. - Exports
RECEIPT_REPO_ROOT, defaulting to that root directory. - Exports
RECEIPT_REPO_KEY, defaulting to the value baked into the wrapper. Both of these are set with the shell’s${VAR:-default}form, so a value you exported first is kept; setRECEIPT_REPO_KEYyourself if you need a different repo key. - Resolves a Bun runtime, in this precedence:
$RECEIPT_BUN_BINif executable, then$BUN_BINif executable, thenbunonPATH, then$BUN_INSTALL/bin/bun, then$HOME/.bun/bin/bun. execs that Bun binary on<root>/packages/receipt-app/src/cli.ts, falling back to<root>/src/cli.tsif the first path does not exist.
bun run receipt:cli runs ./.receipt/bin/receipt, and bun run receipt:factory runs ./.receipt/bin/receipt factory.
Bare receipt, help, and exit behaviour
- With no command at all, and with both stdin and stdout attached to a TTY, the CLI applies the saved session’s environment defaults and opens the Factory board. Non-interactively, it prints the usage text instead. The released binary does the opposite in both cases: a bare
receiptthere goes straight to connector onboarding, starting a browser sign-in first unless a live session is already saved. help,--helpor-has the first token prints usage.receipt connect --helpprints the separate Receipt Connect usage.- After the command settles, the CLI flushes stdout, then stderr, then exits. Set
RECEIPT_CLI_NO_FORCE_EXIT=1to disable that forced exit. - An uncaught error prints exactly
error: <message>to standard error and sets exit code 1.
Environment loading, and what wins
Environment files are loaded at module load, before anything else is imported, so the values are in place for every later import. First the CLI finds the repository root: it walks up from the current working directory, then up from the module’s own directory, and takes the first directory that contains all three ofpackage.json, packages/receipt-app/, and apps/start/. If no such directory is found, nothing is loaded at all — which is what makes the same code safe inside a released binary.
Files are then read in two passes, in this order:
Missing files are silently skipped, and the generated files are de-duplicated by resolved path.
Two rules govern precedence, and they are not the same rule:
- Later files override earlier files. A value set in
apps/start/.env.localbeats the same value in<repo>/.env. - A value already exported in your shell always wins. The set of protected keys is snapshotted once, before any file is read — so a variable an earlier file introduced is not protected, and a later file can still override it.
receipt picks up a one-off RECEIPT_CONNECT_GATEWAY_URL=… receipt … without you editing any file.
The
.env parser accepts KEY=value with an optional leading export, skips blank and # lines, and silently ignores any line that does not match. Double-quoted values unescape \n, \r, \t, \" and \\; single-quoted values are taken literally; unquoted values have a trailing #comment stripped and are then trimmed.Argument parsing
The parser is small and has no short flags other than-h:
--key=valueand--key valueboth setkey.--keyat the end of the arguments, or followed by another--…token, setskeyto booleantrue.- A repeated flag accumulates into an array. Accessors that expect a single value take the last one.
- A bare
--stops flag parsing; everything after it becomes a positional argument. A bare--at the very front is dropped before the first token is taken as the command.
--export, --env and --print-env on receipt connect token are compared against true and nothing else, so --export=true is ignored. Most, including --json on the read commands, also accept the string "true". A few, such as --receipt-replay and --include-receipts on receipt debug prod, also accept 1, yes or on. Pass the bare flag and you will always get the behaviour you expect.
workspace, tools and mcp are handled by separate sub-CLIs. The dispatcher re-serialises your arguments before handing them over: every flag is moved behind the positional arguments, and a repeated flag is re-emitted as one comma-joined value. Those sub-CLIs have their own simpler parser, which keeps only the last value of a repeated flag.What this CLI has that the released binary does not
The dispatcher has 27 top-level commands. These sixteen exist only here:whoami, new, dev, run, trace, replay, inspect, fork, dst (undocumented alias simulate), eval, jobs, abort, memory, sessions, debug, factory
plus four extras under connect: import-local-aws, import-local-github, nango, and token (aliases jwt, mint). receipt connect doctor and receipt connect check also mean something else here — they run an integrations health probe that requires a webhook secret in the environment, not the released binary’s read-only endpoint check.
Everything else — setup, login, logout, doctor, connect, workspace, tools, mcp, import, observe — shares a name with the released binary. An unknown command throws Unknown command '<command>'.
The usage text groups the surface as User commands, Developer commands, Worker/debug commands and Legacy agent-framework commands; new, dev and run are the legacy group.
proxy is still a case in the dispatcher but always throws. It has been removed.Signing in
receipt login, receipt logout and receipt whoami manage the same session store the released binary uses: ~/.receipt/session.json, or $RECEIPT_CLI_SESSION_FILE if set, inside $RECEIPT_CLI_CONFIG_DIR if set. A login also writes a per-target file, session.<target>.json for prod, dev or local — unless RECEIPT_CLI_SESSION_FILE is set, which suppresses per-target files entirely.
Signing in with one program therefore signs you in for the other.
At the top of every command, the saved session seeds environment defaults, but only for variables that are not already set:
This is why commands that demand an actor identity —
receipt memory, for one — usually work after a login without any flags. It is also why an explicit environment value still wins, so CI and one-off commands can target another gateway.
receipt whoami prints the target, workspace and gateway and never prints the token; with nothing saved it prints receipt whoami: not signed in. receipt logout deletes the session file or files and unsets RECEIPT_CONNECT_TOKEN in the current process, printing receipt logout: removed saved CLI session or receipt logout: no saved CLI session found.
receipt doctor
--repo-root defaults to the current working directory, which the wrapper has already set to the repo root.
What it probes
- Five binaries —
bun,git,gh,awsandcodex.git,gh,awsandcodexare resolved from their own override variable (RECEIPT_GIT_BIN,RECEIPT_GH_BIN,RECEIPT_AWS_BIN,RECEIPT_CODEX_BIN) before falling back to a lookup onPATH.bunis resolved fromRECEIPT_BUN_BIN, or else from the Bun runtime the CLI is already running under, so it never reaches that lookup. Each resolved binary is then run with--version, and the first non-empty output line is reported as its version. - The git repository — only when
gitresolved. It reads the top level, the current branch, the porcelain status, and the remotes. - The Factory config — it reads
<repo root>/.receipt/config.jsonand reports the path and whether it is present. A file that is present but unparsable still counts as present; it just yields no data directory. - The data directory —
$RECEIPT_DATA_DIR, then$DATA_DIR, then the config’sdataDir, then<repo root>/.receipt/data. - GitHub and AWS authentication —
gh auth statusandaws sts get-caller-identity --output json.
Blocking versus warning
Exactly four conditions are blocking. These, and only these, setok to false:
ok:
gh or aws binary is not a separate blocker — the missing-binary error is folded into the corresponding auth warning.
It never sets a non-zero exit code
null:
binaries carries ok, path, source (override, runtime or lookup), version and error. In text mode the first line is doctor: ok or doctor: blocked.
There is no model-key check
The text output always prints this fixed line, whatever your configuration:receipt doctor performs no check of any model API key and has no openAiApiKey field in its report, despite what older repo markdown claims. Model credentials resolve from organization BYOK rows, not from a process variable.
One binary, three --output-file envelopes
Do not assume a single shape when you script against --output-file:
In text mode the first two print
wrote <abs path> instead. receipt tools has no output mode to switch — its --json is the argument payload for tools call — so it always prints the envelope above. The path is resolved to an absolute path and its parent directories are created for you. Without --output-file, the payload goes to standard output.
Next step: read receipts and drive jobs.