Skip to Content
ReferenceCLI commands

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

FlagAliasEffect
--help-hprint the logo, then the command list or a command’s options
--version-vprint 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]]
ArgumentTypeDefaultMeaning
projectpositionalcurrent directorydirectory to open; a missing directory exits 1
--resume, -rstring—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>]
FlagAliasTypeDefaultMeaning
message—positional (variadic)[]the prompt; words are joined with spaces
--model-mstringconfig’s current modelprovider/model, e.g. anthropic/claude-opus-4. Without a slash it changes only the model
--agent—stringbuildplan | build | review | explore | danger
--continue-cbooleanfalsecontinue the most recent active session for this directory
--session-sstring—continue a specific session id
--max-turns—numberunboundedcap on agent iterations; loop-health and the gates are the only limit without it
--yes-ybooleanfalseanswer 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 default build mode every mutating tool (write, edit, bash) defaults to ask, so a bare freecode 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 serve

No 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]
FlagTypeDefaultMeaning
--portnumber4096port to listen on
--hoststring127.0.0.1interface to bind
--openbooleantrueopen the browser; --no-open to suppress
--require-authbooleanfalsedemand 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>
CommandArgumentMeaning
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
CommandMeaning
graph statsnode/edge/vector counts for the derived memory graph
graph rebuildrebuild vectors and graph from the memory files on disk
ui-installdownload the optional graph explorer addon into ~/.freecode/addons/graph-ui/
ui-uninstallremove 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>]
FlagAliasTypeDefaultMeaning
sessionId—positionalmost recent sessionwhich session to render
--follow-fboolean—re-render live as the session runs
--slow—number—only show model calls slower than N ms
--tools—booleantrueinclude 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_ENDPOINTship 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]
FlagAliasTypeDefaultMeaning
suite—positionaltrajectorysuite name, resolved as evals/<suite>.jsonl
--trials—number1, or 3 under --gateruns per case; 3 enables majority-of-3. An explicit --trials 1 under --gate is honoured with a warning
--model-mstringconfig’s current modelprovider/model override for cases that don’t pin one
--gate—booleanfalseexit 1 on regression against the recorded baseline
--json—booleanfalseemit { report, verdict } instead of the per-case lines
--quarantine-report—booleanfalseprint 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—booleanfalsewith --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—booleanfalserecord 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>
CommandArgumentsNotes
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.

CommandFlagNotes
login--no-browserskip 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]
FlagAliasDefaultMeaning
--purge—falsealso delete ~/.freecode — sessions, memory, history, usage
--dry-run—falseprint what would be removed, delete nothing
--force-f, -y, --yesfalseskip 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.

  • session CLI covers 2 of 12 operations. fork, switch, archive, export, import, upload, download all exist over IPC and none has a CLI surface, so scripting session management means speaking JSON-RPC by hand.
  • eval only works from the repo root. evalsDir() is path.resolve("evals") (eval/dataset.ts:19), so the suite is found relative to the current working directory and freecode eval from anywhere else fails with “no such suite”. The shipped cases also reference FreeCode’s own source paths, which nothing in --help says.
  • eval --gate does not imply --trials 3. The default is one trial, which is pass@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.
  • uninstall ignores the variables the installer honours. It hard-codes ~/.freecode and four Unix bin paths, while install.sh supports FREECODE_HOME and FREECODE_INSTALL_DIR, and the Windows launcher path is not in the list at all.