Skip to Content
ReferenceEnvironment variables

Environment variables

Environment variables are the right lever for two situations a config file handles badly: one-off debugging (FREECODE_DEBUG=1 freecode run "…") and CI, where there is no home directory worth persisting to.

Nothing here is required. Every variable has a working default, and the two things you might genuinely need — an API key and a model — are better kept in ~/.freecode/config.json.

Two things to know before using the tables:

  1. When a variable is read matters. Most are read per call, so exporting one in a long-running freecode serve takes effect on the next turn. A few are captured once at process start and need a restart. The “Read” column says which.
  2. 0 means different things in different rows. For timeouts it disables the guard; for FREECODE_READ_DEDUP and FREECODE_CACHE_MISS_NOTICES it turns a feature off; for FREECODE_IDLE_NUDGE_TOKENS it disables the nudge. Nothing here treats an unparseable value as 0 — a typo falls back to the default, because silently disabling a cost guard is the worse failure.

Credentials

VariableUsed by
ANTHROPIC_API_KEYAnthropic provider
OPENAI_API_KEYOpenAI provider
GEMINI_API_KEYGemini provider
MINIMAX_API_KEYMiniMax (also covers minimax-coding-plan)
DEEPSEEK_API_KEYDeepSeek
ZAI_API_KEYZ.ai
BRAVE_API_KEYwebsearch uses the Brave API when set; DuckDuckGo’s keyless HTML endpoint otherwise

config.json wins over the environment. getApiKey() checks providers.<id>.apiKey first and only then the variable, so if a key is saved in the app, exporting a different one changes nothing. A provider suffixed with -coding-plan falls back to its base provider’s key and variable.

VariableValuesEffect
FREECODE_ANTHROPIC_AUTHoauth, api-keypins how anthropic authenticates, overriding providers.anthropic.authMode

oauth bills a Claude Pro/Max subscription instead of a key, and setting it is one of the three ways to opt in. It is not a neutral switch — see Anthropic subscription login for what the OAuth path does and the risk it carries.

Provider requests

VariableDefaultReadEffect
FREECODE_HEADER_TIMEOUT_MS300000per requesthow long a provider may take to return response headers. Cleared the instant they arrive, so it never caps generation time. 0 disables
FREECODE_SSE_STALL_TIMEOUT_MS180000per requesthow long a live stream may be completely silent. Not a token budget: a slow model that emits keep-alives is healthy. 0 disables
FREECODE_CACHE_TTL5mper call5m or 1h. 1h opts into Anthropic’s extended prompt cache. Any other value warns and falls back to 5m
FREECODE_CACHE_MISS_NOTICESonper callset 0 to stop surfacing “prompt cache went cold / missed” notices
FREECODE_DEBUG_CACHEoffper call1 logs a fingerprint of every cacheable prompt segment, for diagnosing a low hit rate

Both timeouts live in a fetch wrapper below the AI SDK (providers/fetch-timeout.ts) so every SSE byte counts. Measuring liveness above the SDK’s normalizer counts only 5 part types and misses tool-input-delta, which made a large write look like a dead stream.

FREECODE_FIRST_CHUNK_TIMEOUT_MS, FREECODE_STREAM_STALL_TIMEOUT_MS, and FREECODE_REQUEST_TIMEOUT_MS were the previous generation of this guard and no longer exist. If you have them in a shell profile, delete them — they do nothing.

Context and spend

VariableDefaultReadEffect
FREECODE_COMPACT_TARGET_TOKENS120000per callthe cost ceiling compaction aims for. Raise it to keep more history; lower it to compact aggressively on a small quota
FREECODE_AUTO_COMPACT_TOKENSunsetper callforce auto-compaction at N tokens instead of just below the model’s window. Mostly a testing lever — the real trigger is unreachable by hand
FREECODE_MAX_TURN_TOKENSunsetper callper-run spend circuit breaker in billed tokens (input + output). Off by default; this is a backstop for runaway oscillation, not an operating limit
FREECODE_IDLE_NUDGE_TOKENS100000per call (TUI)context size below which the “your cache has gone cold” nudge never fires. 0 disables it

Values must be positive integers; anything else logs a warning and keeps the default, because a typo here looks exactly like compaction being broken.

Tools

VariableDefaultReadEffect
FREECODE_TOOL_RESULT_BUDGET_CHARS200000process starttotal characters of tool results kept verbatim in context before older ones are replaced with a marker
FREECODE_OUTPUT_MAX_CHARS30000process startchars of a single tool output shown to the model (head + tail)
FREECODE_OUTPUT_TAIL_CHARS6000process starthow much of that budget is spent on the tail. Head is the remainder
FREECODE_OUTPUT_LINES200process startdefault line window per output call
FREECODE_OUTPUT_STORE_BYTES16777216process startper-session output store budget; oldest outputs evict first
FREECODE_OUTPUT_STORE_SESSIONS50process starthow many per-session stores stay live
FREECODE_READ_DEDUPonper call0 makes read send the full file body every time instead of eliding an unchanged re-read
FREECODE_READ_LINE_NUMBERSoffper call1 restores the per-line N: prefix on read output; the range footer stays either way. Off by default since the D3 A/Bs measured −10.9%/−6.8% tokens with quality unchanged
FREECODE_BASH_COMPRESSoffper call1 turns on content-aware compression of large bash output: build-log noise collapses, search matches and diffs never do. Off until its A/B earns the default
FREECODE_LSP_SERVERSunsetper callJSON map of extension → server, e.g. {".rs":{"command":"rust-analyzer","args":[]}}. Extends the built-in registry
FREECODE_RTKonper call0 disables the built-in hook that rewrites ls-style bash commands into compact rtk equivalents
FREECODE_TASK_NOTIFYonper spawn0 stops finished background sub-agents and shells from notifying the agent, so no turn starts without your input. agent(run_in_background) then runs in the foreground; background shells still run. Beats tasks.notify in settings.json. See long-running commands

Memory

VariableDefaultEffect
FREECODE_DISABLE_MEMORY_EXTRACTIONunset1, true, or yes turns off automatic extraction. Overrides memory.autoExtract in settings.json

Loop health

VariableDefaultEffect
FREECODE_DISABLE_REDIRECTunset1, true, or yes turns off trajectory redirection. Overrides redirect.enabled in settings.json; the feature is off by default anyway, so this is the kill switch for a project that turned it on

Evals

VariableDefaultEffect
FREECODE_EVALS_DIR./evalswhere freecode eval looks for <suite>.jsonl and quarantine.txt. The default is resolved against the current working directory, so set this to run the suite from anywhere but the repo root
FREECODE_EVAL_TRIAL_TIMEOUT_MS300000wall-clock cap per trial. The backstop for a trial that blocks on something without a timeout of its own; a trial that trips it is a failed trial, not a crashed suite
FREECODE_EVAL_VERIFY_TIMEOUT_MS60000cap on a coding case’s verify command, spawned in the sandbox after the turn
FREECODE_EVAL_KEEP_SANDBOXunset1 keeps the case’s tmpdir instead of removing it, and prints the path — how you reproduce a red coding case by hand
FREECODE_EVAL_HOME~/.freecodewhere eval_report.json (last run) and eval_runs.jsonl (history) are written. Changing it between runs resets the gate’s baseline
FREECODE_JUDGE_PROVIDERunsetprovider that grades the judged suite. Required for it and for pnpm eval:gate; with none set the cases still run but the gate closes
FREECODE_JUDGE_MODELprovider defaultjudge model. Must not resolve to the model under test — a collision throws before any case runs

See eval harness.

Diagnostics

VariableEffect
FREECODE_DEBUGany non-empty value enables logger.debug output. All logs go to stderr — core speaks JSON-RPC on stdout, so logging there would corrupt the protocol
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTOTLP endpoint used by freecode trace --otlp when no URL is passed. Checked before the next row
OTEL_EXPORTER_OTLP_ENDPOINTsame, the generic form
OTEL_EXPORTER_OTLP_HEADERScomma-separated k=v list, per the OTLP spec — this is how you pass a Langfuse or Phoenix auth header

Traces are exported from the recorded event log, never from the hot path, so a slow collector cannot slow down a turn.

Paths and packaging

VariableSet byEffect
FREECODE_BUNDLEDthe release binary (build:bun)1 means “self-contained binary”: core is bundled rather than spawned from disk, the background update check runs, and the native-library re-exec happens
FREECODE_BUILD_VERSIONthe buildthe version reported by --version and the TUI. Falls back to package.json, then unknown
FREECODE_NO_UPDATEyou1, true, or yes disables the background update check, so no “update available” line appears. Nothing self-installs either way — a version pins itself. An explicit freecode update still updates
FREECODE_ROOTyoumonorepo root used by the TUI and the VS Code extension to locate core when it is not bundled. Set it if you run a frontend from outside the repo
FREECODE_HOMEyouonly the updater’s builds/stable/freecode lookup honors this. It does not relocate your data — see known gaps
CLAUDE_CONFIG_DIRyouwhere Claude Code sessions are read from for the import tab. Defaults to ~/.claude; read-only, FreeCode never writes there
CDP_URLyouChrome DevTools endpoint for the legacy browser path. Defaults to http://localhost:9222
FREECODE_BUN_TARGET, FREECODE_BUN_OUTFILECIcross-compile target and output path for pnpm build:bun. Contributor-facing only

__FREECODE_REEXECED is an internal sentinel the binary sets on itself to avoid a re-exec loop when it re-launches with the native-library path set. Do not set it.

Not read — exported

Hook commands receive CLAUDE_SESSION_ID, CLAUDE_TOOL_NAME, CLAUDE_TOOL_INPUT, CLAUDE_CWD, CLAUDE_AGENT_ID, and CLAUDE_AGENT_TYPE in their environment. FreeCode sets those for your hook; setting them yourself does nothing. See hook events.

Known gaps

Found while writing this page; each is also tracked in TODO.md.

  • FREECODE_HOME looks like a data-root override and is not one. It is read in exactly one place — the updater’s stable symlink path (apps/tui/src/entry.ts:101). Every other path (config.json, sessions/, projects/, rollout/, history.jsonl) is built from os.homedir() directly, so setting it produces a half-relocated install. Either honor it everywhere through one freecodeHome() helper, or rename it to what it actually controls.
  • Six variables need a restart, and nothing says so. The tools/output-store constants and TOOL_RESULT_BUDGET_CHARS are module-load consts, while the compaction and cache variables are read per call — a deliberate choice there (“the long-lived daemon and the tests can both change it without a module reload”) that was simply not applied to the tool budgets.
  • FREECODE_TOOL_RESULT_BUDGET_CHARS="" silently means zero. Number("") is 0, which is finite, so an empty export sets the budget to 0 instead of falling back to the default the way every other numeric variable does.
  • No --env-style listing. There is no command that prints the effective values, so diagnosing “why is it compacting so early” means reading source. A freecode config env that dumps name / default / effective / source would pay for itself.