CLI commands
freecode with no subcommand launches the terminal UI. Everything else is a
subcommand.
Every command lives in one yargs chain (apps/core/src/cli/create-cli.ts), with
one file per command under cli/commands/. The chain is .strict(), so an
unknown command or an unknown flag is an error rather than something silently
ignored — if a flag is not on this page, it does not exist.
Two commands are injected by the binary rather than by core (apps/tui/src/entry.ts):
the default $0 TUI command and update. That is why freecode --help from the
installed binary shows two more commands than the core package does.
Global flags
| Flag | Alias | Effect |
|---|---|---|
--help | -h | print the logo, then the command list or a command’s options |
--version | -v | print the version |
The version comes from FREECODE_BUILD_VERSION when set (baked into release
binaries), otherwise from package.json, otherwise the literal string unknown.
freecode — the TUI
freecode [project] [--resume [id]]| Argument | Type | Default | Meaning |
|---|---|---|---|
project | positional | current directory | directory to open; a missing directory exits 1 |
--resume, -r | string | — | resume a session by id; pass the flag with no id to open the session picker |
The project path is resolved and chdir’d into before anything else starts, so
project-scoped settings, skills, and commands are read from there.
In a release binary an update check runs shortly after the TUI is on screen
— one GitHub API call with a 3-second timeout, never awaited and never on the
path to first paint. If a newer release exists, an update available (vX.Y.Z) · run freecode update line appears under the version in the header; nothing is
installed until you run freecode update. Any failure —
offline, rate-limited, GitHub down — is swallowed and no line appears.
freecode run — one-shot, headless
freecode run [message..] [--model <p/m>] [--agent <mode>] [--continue] [--session <id>]
[--max-turns <n>] [--yes] [--allow <rule>]| Flag | Alias | Type | Default | Meaning |
|---|---|---|---|---|
message | — | positional (variadic) | [] | the prompt; words are joined with spaces |
--model | -m | string | config’s current model | provider/model, e.g. anthropic/claude-opus-4. Without a slash it changes only the model |
--agent | — | string | build | plan | build | review | explore | danger |
--continue | -c | boolean | false | continue the most recent active session for this directory |
--session | -s | string | — | continue a specific session id |
--max-turns | — | number | unbounded | cap on agent iterations; loop-health and the gates are the only limit without it |
--yes | -y | boolean | false | answer permission prompts with allow. Deny rules and read-only modes still refuse |
--allow | — | string (repeatable) | [] | grant one permission rule for this run only, e.g. --allow 'Bash(pnpm test:*)' |
If no message positional is given and stdin is not a TTY, the prompt is read from
stdin — echo "explain this repo" | freecode run works.
Streams split across two file descriptors. Assistant text goes to stdout;
tool activity, thinking, and errors go to stderr. That is what makes
freecode run "summarize the diff" > out.md produce a clean file. Exit code is
0 when the turn succeeded, 1 otherwise (including “no message provided” and
“no provider configured”).
The turn is unbounded unless you pass --max-turns. That caps iterations,
not spend — use FREECODE_MAX_TURN_TOKENS for a token ceiling in CI.
Read this before scripting
run. In headless mode nobody can answer a permission prompt, and an unanswerable prompt resolves to deny, never to a silent allow. In the defaultbuildmode every mutating tool (write,edit,bash) defaults to ask, so a barefreecode run "fix the test"reads your repository fine and is denied every write.
Three ways to make a headless run able to act, narrowest first:
# 1. Grant exactly what the task needs, for this run only.
freecode run --allow 'Edit' --allow 'Bash(pnpm test:*)' "fix the failing test"
# 2. Answer every prompt with allow. Deny rules and read-only modes still refuse.
freecode run --yes "fix the failing test"
# 3. Persist the grants for the repo, in .freecode/settings.json.
# { "permissions": { "allow": ["Edit", "Write", "Bash(pnpm test:*)"] } }
freecode run "fix the failing test"--yes answers the ask tier and nothing else. A deny rule is still absolute,
and --agent plan|review|explore is still read-only — those are decisions someone
already made, not questions waiting for an answer. --agent danger remains the
only flag that bypasses evaluation entirely; use it only in a sandbox.
freecode serve — the backend alone
freecode serveNo flags. Starts the JSON-RPC 2.0 backend on stdin/stdout — the same process the TUI spawns internally, and what any other frontend attaches to. See IPC methods.
settings.json hooks and the built-in rtk rewrite hook load here through
hooks/bootstrap.ts — the same bootstrap freecode run calls, so a headless run
and a served session see the same hooks.
freecode web — browser frontend
freecode web [--port 4096] [--host 127.0.0.1] [--open] [--require-auth]| Flag | Type | Default | Meaning |
|---|---|---|---|
--port | number | 4096 | port to listen on |
--host | string | 127.0.0.1 | interface to bind |
--open | boolean | true | open the browser; --no-open to suppress |
--require-auth | boolean | false | demand the bearer token even on loopback |
Auth is bind-dependent: on a non-loopback host a token is always required, and
--require-auth extends that to loopback too. The process then blocks forever —
Ctrl+C is how you stop it.
freecode mobile — phone pairing wizard
freecode mobile [--port 4096]The guided version of freecode web --host <tailnet-name>: checks, installs,
starts and signs into Tailscale, resolves the MagicDNS hostname, mints a token,
prints a pairing QR code, and confirms when a device actually connects. If the
Tailscale setup does not complete, nothing is started and the exit code is 1.
freecode session
freecode session list [--project <path>]
freecode session delete <sessionId>| Command | Argument | Meaning |
|---|---|---|
list | --project <path> | list sessions, optionally filtered to one project path |
delete | <sessionId> (required) | delete a session |
Sub-commands are required — freecode session alone prints the help and exits
non-zero. Fork, archive, export, and import exist in the backend but have no CLI
surface yet (known gaps).
freecode memory
freecode memory graph stats [--project <path>]
freecode memory graph rebuild [--project <path>]
freecode memory ui-install [--addon-version <tag>]
freecode memory ui-uninstall| Command | Meaning |
|---|---|
graph stats | node/edge/vector counts for the derived memory graph |
graph rebuild | rebuild vectors and graph from the memory files on disk |
ui-install | download the optional graph explorer addon into ~/.freecode/addons/graph-ui/ |
ui-uninstall | remove that addon |
--project defaults to the current directory. The explorer is an opt-in
(~280 KB) download, which is why graph.explore over IPC answers
{ error: "not-installed" } rather than starting an empty server. Background:
memory knowledge graph.
freecode trace
freecode trace [sessionId] [--follow] [--slow <ms>] [--tools] [--json] [--list] [--otlp <url>]| Flag | Alias | Type | Default | Meaning |
|---|---|---|---|---|
sessionId | — | positional | most recent session | which session to render |
--follow | -f | boolean | — | re-render live as the session runs |
--slow | — | number | — | only show model calls slower than N ms |
--tools | — | boolean | true | include tool calls in the waterfall; --no-tools for model calls only |
--json | — | boolean | — | emit the assembled trace as JSON |
--list | — | boolean | — | list recorded sessions instead of tracing one |
--otlp | — | string | $OTEL_EXPORTER_OTLP_ENDPOINT | ship the trace to an OTLP collector (Langfuse, Phoenix, Jaeger) |
The trace is folded from the rollout event log, so it works on sessions that have
already finished — and on ones that hung, which is the point: a model.request
with no matching response is the evidence. See tracing.
freecode eval
freecode eval [suite] [--trials <n>] [--model <p/m>] [--gate] [--json] [--quarantine-report]
freecode eval ab <suite> --baseline <spec> --candidate <spec> [--trials <n>] [--cases a,b]
freecode eval add <session-id> [--turn <n>] [--suite <name>] [--write]| Flag | Alias | Type | Default | Meaning |
|---|---|---|---|---|
suite | — | positional | trajectory | suite name, resolved as evals/<suite>.jsonl |
--trials | — | number | 1, or 3 under --gate | runs per case; 3 enables majority-of-3. An explicit --trials 1 under --gate is honoured with a warning |
--model | -m | string | config’s current model | provider/model override for cases that don’t pin one |
--gate | — | boolean | false | exit 1 on regression against the recorded baseline |
--json | — | boolean | false | emit { report, verdict } instead of the per-case lines |
--quarantine-report | — | boolean | false | print quarantine promotion/demotion proposals and exit without running anything |
--save | — | string | — | write this run’s report to a file, for a later --compare |
--compare | — | string | — | diff against a saved report; exits 1 if the criterion is not met |
--stuck | — | boolean | false | with --compare, also require repetition to fall |
--otlp | — | string | — | ship the scores to a collector, linked to the traces they graded; empty falls back to OTEL_EXPORTER_OTLP_ENDPOINT |
--accept-baseline | — | boolean | false | record a failing run as the new baseline and exit 0 — for when the suite was re-scoped, not when the agent got worse |
eval ab runs two variants over the same cases, interleaved, and is
deliberately never a gate: no baseline, no history, always exits 0. eval add
harvests a draft case out of a recorded session (stdout for the case, stderr for
the notes); it refuses the coding suite, which needs a files fixture no
recorded session has.
Each case drives a real agent turn against a real provider, so this costs
money and takes minutes. Results are written to ~/.freecode/eval_report.json
(last run) and appended to ~/.freecode/eval_runs.jsonl (history);
FREECODE_EVAL_HOME relocates both. The judged suite additionally needs
FREECODE_JUDGE_PROVIDER — it refuses to run when the judge resolves to the
model under test, and closes the gate when no judge is configured at all.
The suite path is resolved relative to the current working directory, so this
only works from the repo root unless FREECODE_EVALS_DIR is set
(known gaps). See eval harness.
freecode mcp
freecode mcp list
freecode mcp add <name> <local|remote> <command-or-url>
freecode mcp remove <name>
freecode mcp start <name>
freecode mcp stop <name>
freecode mcp status <name>| Command | Arguments | Notes |
|---|---|---|
list | — | reads config only; it does not connect. The output says so |
add | <name> <type> <command> | type is local or remote. For local the command string is split on spaces; for remote it is the URL |
remove | <name> | |
start / stop / status | <name> |
Servers are written to ~/.freecode/config.json under mcp.servers, with
enabled: true and timeout: 5000 defaults. There is no project-scoped MCP
config (known gaps).
freecode auth
freecode auth login [provider] [--no-browser]
freecode auth status
freecode auth logout [provider]Authenticates the anthropic provider with a Claude Pro/Max subscription
instead of an API key. provider defaults to anthropic and any other value is
rejected by name — it is the only provider with an OAuth mode.
| Command | Flag | Notes |
|---|---|---|
login | --no-browser | skip opening the authorize URL; print it and paste the code back |
status | — | auth mode, token expiry, scopes; also reports an importable Claude Code login |
logout | — | deletes the stored tokens and un-pins the mode, reverting to your API key |
login prints a disclosure before it does anything, and it means it: reaching
subscription inference requires presenting FreeCode to Anthropic as Claude
Code — its OAuth client id, its headers, its identity line. Anthropic reserves
that inference for its own surfaces and has acted against tools doing this, and
the account at risk is yours. The full stance, the three opt-ins, and what
happens when Anthropic refuses are on
Anthropic subscription login.
The flow is PKCE against a localhost callback with a 120-second wait, falling
back to paste. It is a single process — the OAuth state is the PKCE
verifier, so there is no second command and no --code flag. Tokens go to
~/.freecode/auth.json at mode 0600, separate from config.json, and refresh
automatically.
freecode update
Runs curl -fsSL https://freecode.website/install | bash and exits with the
installer’s status. Injected by the binary, so it is not present when running core
from source.
freecode uninstall
freecode uninstall [--purge] [--dry-run] [--force]| Flag | Alias | Default | Meaning |
|---|---|---|---|
--purge | — | false | also delete ~/.freecode — sessions, memory, history, usage |
--dry-run | — | false | print what would be removed, delete nothing |
--force | -f, -y, --yes | false | skip the confirmation prompt |
The program goes, the data stays. By default it removes a freecode binary
found in /usr/local/bin, /usr/bin, ~/.local/bin, or ~/.cargo/bin, plus
~/.freecode/builds (the installed versions). Your sessions, rollout logs,
memory, prompt history, and usage stay where they are.
--purge is what takes ~/.freecode entirely, and the confirmation names what
is inside it rather than saying only “proceed”. Nothing is backed up either way,
so copy ~/.freecode/projects/ and ~/.freecode/sessions/ before purging if you
might want them back.
It prints the list first and asks y/N unless --force is given; a
non-terminal stdin is an error telling you to pass --force, not a silent “no”.
Exit code is 1 if any removal failed. These are the same semantics as
curl -fsSL https://freecode.website/uninstall | bash, which has always drawn
the line here.
Known gaps
Found while writing this page; each is also tracked in TODO.md.
sessionCLI covers 2 of 12 operations.fork,switch,archive,export,import,upload,downloadall exist over IPC and none has a CLI surface, so scripting session management means speaking JSON-RPC by hand.evalonly works from the repo root.evalsDir()ispath.resolve("evals")(eval/dataset.ts:19), so the suite is found relative to the current working directory andfreecode evalfrom anywhere else fails with “no such suite”. The shipped cases also reference FreeCode’s own source paths, which nothing in--helpsays.eval --gatedoes not imply--trials 3. The default is one trial, which ispass@1— the statistic the gate’s own design calls too noisy to block on.- MCP config is user-scope only.
getConfigDir()is hard-wired to~/.freecode, so a repository cannot ship the MCP servers its contributors need the way it can ship permission rules and hooks. uninstallignores the variables the installer honours. It hard-codes~/.freecodeand four Unix bin paths, whileinstall.shsupportsFREECODE_HOMEandFREECODE_INSTALL_DIR, and the Windows launcher path is not in the list at all.