Skip to content

Environment

Every environment variable that changes how the office behaves.

23 min readUpdated 13 Sept 2026Reviewed 12 Sept 2026Published 12 Sept 2026/docs/reference/environment

Fifty-three DEV3D_* variables plus HOST and PORT are everything the orchestrator reads from its environment. Each row below gives the default the code actually falls back to and what the value does — not a restatement of its name.

All reads go through three helpers: a string, a number, and a path. They behave the same way everywhere, and the two behaviours that surprise people are worth stating once. An empty string counts as absent, so DEV3D_LOG_LEVEL= is exactly the same as not setting it. A non-finite number silently falls back, so DEV3D_MAX_CONCURRENCY=lots gives you 4 rather than an error or a crash. Paths are resolved against the repository root, not against the working directory, so ./data/dev3d.sqlite means the same thing no matter where you started the process.

The complete list

VariableDefaultWhat it does
HOST127.0.0.1The interface the HTTP and WebSocket server binds to. Loopback by default, which means the office is not reachable from the network until you say so.
PORT8787The port. A bind failure on this port is reported by name: Port 8787 on 127.0.0.1 is already in use.
DEV3D_LLM_MODEautomock runs the whole engine against scripted employees with no billing; live requires real providers; auto resolves to live the moment any provider counts as configured. Anything unrecognised falls back to auto.
DEV3D_ROUTINGbalancedThe installation's default routing posture — cheap, balanced or quality. It is only a default: a floor can override it, and the engine reads the floor's posture when it has one.
DEV3D_LOG_LEVELinfoOne of debug, info, warn, error. Anything else falls back to info. debug is what turns on the per-provider discovery and health lines.
DEV3D_DB./data/dev3d.sqliteThe SQLite file holding the office, its runs, turns, events and approvals. Environment-only: it has to be known before anything can be loaded from it.
DEV3D_SKILLS_DIR./skillsThe directory the skill loader reads *.md from at boot. Created if missing. A malformed skill file is skipped with its reason and never fatal.
DEV3D_AUTO_APPROVE_SHELLfalseOnly the literal true enables it, and what it enables is run_shell skipping the human approval round trip entirely.
DEV3D_APPROVAL_TIMEOUT_MS600000How long an approval waits before the engine treats it as refused, floored at 1000. A timeout is a refusal that the run can act on rather than a wedged run.
DEV3D_SOFT_SPEND_APPROVAL_USD1.5Ask a human before a run's spend crosses this, in USD. 0 disables the gate. It fires once per run, between stages, and a refusal cancels the run.
DEV3D_MAX_CONCURRENCY4How many employees may execute at once inside one stage, clamped to 1..16. It applies to parallel stages, review-loop reviewers and review-loop revisions — nothing else is concurrent.
DEV3D_RUN_BUDGET_USD5Read into the config and never used. See Budget and limits below: a run's real ceiling is the floor's budget.defaultRunUsd, which also defaults to 5, which is why this has gone unnoticed.
DEV3D_WORKSPACE./workspaceThe default project directory: what a run uses when it names no floor of its own, and what the first organisation is created pointing at.
DEV3D_WORKSPACES_ROOT./workspacesWhere a new project folder is created when you create one by name. A name is resolved under this root, which is what stops "create a project" from becoming a way to hand employees the whole disk.
DEV3D_ALLOW_EXTERNAL_WORKSPACEStrueWhether a floor may point at an absolute path outside the workspaces root. Only the literal false disables it — any other value, including nonsense, leaves it enabled.
DEV3D_PLUGINS_DIR./pluginsThe bundled plugin directory. Every subdirectory holding a plugin.json is one plugin, loaded with source label bundled.
DEV3D_PLUGIN_INSTALL_DIR./data/pluginsWhere downloaded plugins land, loaded with source label marketplace. Kept out of the bundled directory so a download cannot be confused with something that shipped.
DEV3D_ALLOW_PLUGIN_INSTALLfalseThe install gate. Only the literal true opens it; until then install() refuses before it fetches anything.
DEV3D_MODEL_DISCOVERYtrueWhether the office asks each configured provider what models it serves. Only the literal false turns it off. mock mode never discovers, whatever this says.
DEV3D_MODEL_DISCOVERY_TTL_MS21600000 (6 hours)How long a discovered list stays fresh before being asked for again. 0 re-asks every time.
DEV3D_MODEL_DISCOVERY_CACHE./data/model-discovery.jsonWhere discovered lists are cached between restarts. The literal values off or none disable the cache, at the cost of a network round trip per provider on every boot.
DEV3D_POOLED_QUALITY_KEY_VARARTIFICIAL_ANALYSIS_API_KEYThe name of the variable holding the pooled-quality key. Only the name is stored, so a secret never reaches a settings document, a log line or the browser.
DEV3D_POOLED_QUALITY_CACHE./data/pooled-quality.jsonWhere pooled benchmark scores are cached. off or none disables it.
DEV3D_POOLED_QUALITY_TTL_MS604800000 (7 days)How long a pooled index stays fresh. A week because a benchmark index moves on the scale of weeks and the free API is rate-limited per key.
DEV3D_BENCHMARKStrueWhether to read OpenRouter's benchmark aggregation for pooled quality. Only the literal false disables it. Needs OPENROUTER_API_KEY to do anything.
DEV3D_BENCHMARK_CACHE./data/benchmarks.jsonWhere the OpenRouter benchmark payload is cached. off or none disables it.
DEV3D_BENCHMARK_TTL_MS86400000 (24 hours)How long that payload stays fresh. A day, because benchmarks are re-run continuously but an index that matters for routing does not move hour to hour.
DEV3D_ENDPOINT_HEALTHtrueWhether upstream endpoint uptime is tracked as a routing signal. Only the literal false disables it.
DEV3D_ENDPOINT_HEALTH_PROVIDERSopenrouterA comma-separated list of provider ids whose models are OpenRouter slugs underneath, and are therefore the only ones that can be asked about. Entries are trimmed and empties dropped.
DEV3D_ENDPOINT_HEALTH_CACHE./data/endpoint-health.jsonWhere uptime readings are cached. off or none disables it.
DEV3D_ENDPOINT_HEALTH_TTL_MS600000 (10 minutes)Uptime freshness. Ten minutes because the underlying figure is a rolling 30-minute one, so asking more often returns a number that has barely moved.
DEV3D_DEEPSEEK_BASE_URLhttps://api.deepseek.com/v1Base URL for the DeepSeek provider, including the version segment.
DEV3D_OPENAI_BASE_URLhttps://api.openai.com/v1Base URL for the OpenAI provider.
DEV3D_OPENROUTER_BASE_URLhttps://openrouter.ai/api/v1Base URL for OpenRouter. Requests to it also carry a referer and title header, which is what OpenRouter asks gateways to send.
DEV3D_ANTHROPIC_BASE_URLhttps://api.anthropic.com/v1Base URL for Anthropic, which speaks the Messages API rather than the OpenAI chat-completions shape.
DEV3D_LOCAL_BASE_URLhttp://127.0.0.1:11434/v1A local OpenAI-compatible runtime. Its presence — not its value — is what marks the local provider as keyless and configured.
DEV3D_LOCAL_API_KEYnoneAn optional credential for the local runtime's local provider, for a gateway that wants one but does not need a paid key.
DEV3D_MCPtrueWhether to connect to the MCP servers you have configured. On by default and inert until one is named, so it changes nothing on a fresh install.
DEV3D_MCP_CONFIG./mcp.jsonThe MCP server file. An empty value disables the file, which is how you run from DEV3D_MCP_SERVERS alone.
DEV3D_MCP_SERVERSemptyInline servers as <id>=<command> [args…], separated by ; because arguments routinely contain spaces. Merged with the file, and the file wins on an id collision. Read outside the main config module.
DEV3D_MCP_GRANT_ROLESnoneWho may call an MCP tool: role ids, * for everyone, none, or the legacy shell-roles/run_shell. The default is none — nobody, so connecting a server widens no one's reach until you name the roles. The project's own README prints shell-roles here and is wrong.
DEV3D_MCP_REQUIRE_APPROVALtrueAsk a human the first time a newly connected server is used. Once per server per connection, and a reconnect asks again. Only the literal false turns it off.
DEV3D_VENDOR_DELEGATIONtrueWhether third-party agent harnesses may be engaged at all. On by default and inert until a vendor is configured.
DEV3D_VENDORSemptyInline vendors separated by ;: a preset name (codex, dsh, hermes, openclaw) or <id>=<preset> to name your own. Read outside the main config module.
DEV3D_VENDORS_CONFIG./vendors.jsonThe vendor file, for anything needing a custom command, arguments or transport. An empty value disables the file.
DEV3D_VENDOR_GRANT_ROLESdelegate-rolesWho may hand work to a vendor: delegate-roles, role ids, * or none.
DEV3D_VENDOR_REQUIRE_CAN_DELEGATEtrueAlso require canDelegate on the role itself, so a grant alone is not enough. Only the literal false relaxes it.
DEV3D_MEMORY_VECTORSfalseLoad the sqlite-vec index so recall can re-rank by meaning. Off is a complete configuration rather than a degraded one, and it needs the variable below as well.
DEV3D_MEMORY_EMBEDDINGemptyprovider/model naming where text becomes vectors. Required for semantic recall: DeepSeek and Anthropic serve no embeddings, and a bare model name is ambiguous across vendors. Empty keeps recall lexical.
DEV3D_EVENT_RETENTION_DAYS30Days of event log to keep, pruned once per boot rather than on a timer, because this project runs no background worker. 0 keeps everything. Runs, turns and artifacts are never pruned.
DEV3D_ALLOW_PRIVATE_PANEL_HOSTSfalseAllow a plugin panel's source to be fetched from a private address. Off by default, because the server does the fetching and the answer is drawn on your screen.
DEV3D_OPENROUTER_REFERERhttps://github.com/scarecr0w12/dev3dThe referer sent with OpenRouter requests, which is what OpenRouter asks gateways to identify themselves with.
DEV3D_OPENROUTER_TITLEdev3d officeThe title sent alongside it.

Engine behaviour

DEV3D_LLM_MODE and what auto really does

The mode is resolved once, at boot, into mock or live, and the resolved value is what the office uses for its whole life. auto becomes live if and only if at least one provider counts as configured; otherwise it stays mock. The consequence is worth stating plainly: adding a provider key to .env and restarting silently moves the office from scripted employees to real ones being billed. If you want to look around before spending anything, set mock explicitly.

The office does not just report the mode, it reports why. Every boot line and every OfficeState carries a llmModeReason, and the reason differs by case: DEV3D_LLM_MODE=auto, and deepseek is configured; DEV3D_LLM_MODE=mock forces scripted employees even though deepseek is configured; and, for the honest failure, DEV3D_LLM_MODE=live, but no provider is configured — every turn will fail. That last sentence is a warning rather than a validation error, and it means exactly what it says: forcing live without a key produces a building in which every turn fails.

In mock mode nothing is excluded from the catalog either. The mock adapter serves every provider, so the full model list stays routable and the office remains demonstrable with no keys at all — which is also why discovery is skipped in that mode rather than performing outbound requests that could not change anything.

Logging, skills and the skills directory

DEV3D_LOG_LEVEL sets the threshold for the server's own logger, whose lines look like 12:04:51.882 INFO [discovery] openrouter: 445 model(s) in 812ms. The scope in brackets is the useful part when you are grepping: boot, http, ws, plugins, skills, discovery, benchmarks, pooled, health and office are the ones you will meet. Log level is also one of the eight values folded into installation settings, so changing it in the console changes it for real.

DEV3D_SKILLS_DIR points at the skill library. Skills are loaded at boot before the server starts listening, so a new markdown file needs a restart to appear. The loader creates the directory if it is missing, reads *.md only, sorts by id, and estimates a token cost from the body length when the front matter does not give one. A malformed file is skipped with its reason logged and a one-line summary at the end — a typo in a markdown file is never the reason the office refuses to open.

Shell auto-approval, approval timeout and soft spend

These three are the money-and-permission surface, and each has a sharp edge.

  • DEV3D_AUTO_APPROVE_SHELL skips the approval round trip for run_shell. That round trip is the only gate shell has: the tool's confinement is its working directory, and arbitrary shell text can leave it. The boot log says so out loud — DEV3D_AUTO_APPROVE_SHELL is on: employees may run shell commands without asking. Leave it off unless the office is genuinely unattended.
  • DEV3D_APPROVAL_TIMEOUT_MS is how long a pending approval waits. On expiry it counts as refused, which is deliberate: an approval nothing can answer must not wedge a run forever, so it always resolves and the run gets an honest no. The floor is 1000 ms; there is no ceiling.
  • DEV3D_SOFT_SPEND_APPROVAL_USD is a gate, not a ceiling. It fires at most once per run, only between stages, and only once spend has already crossed the threshold. Approving it lets the run continue to its real ceiling; refusing it cancels the run rather than pausing it. The approval carries kind 'spend', a null turnId, and is attributed to the first stage's owner so it has somewhere to appear in the UI.

Providers and credentials

Five providers are built in: deepseek, openai, openrouter, anthropic and local. Four of them speak the OpenAI chat-completions shape through one adapter; anthropic speaks the Messages API through its own. The five DEV3D_*_BASE_URL variables override where each one is reached, which is the supported way to point the office at a proxy or a self-hosted gateway that implements one of those shapes.

The four credentials, and the indirect fifth

VariableProvider it configures
DEEPSEEK_API_KEYdeepseek
OPENAI_API_KEYopenai
OPENROUTER_API_KEYopenrouter — and the credential the benchmark refresh gates on, for both the OpenRouter benchmark source and the pooled-quality aggregation. It is this variable, not OPENAI_API_KEY.
ANTHROPIC_API_KEYanthropic
the variable named by DEV3D_POOLED_QUALITY_KEY_VARNothing routable. It authorises an outbound benchmark lookup and nothing else.

Keys are read at boot into memory and never leave the process. The only thing the browser is ever told is the name of the variable a provider needs — which is how a provider added by a plugin can be configured without a manifest ever carrying a secret.

A provider with no key is catalogued but not routable

This is the rule that explains most "why is my plugin's model never chosen" questions. In live mode the set of models a turn may be routed to excludes every provider the installation cannot reach — one with no key and no keyless base URL. Its models still appear in the catalogue and in the Models tab, flagged as unconfigured with a hint naming the variable to set; they simply never become candidates. Routing to one would not be a fallback, it would be a guaranteed failed turn. In mock mode there is nothing to exclude, because the mock adapter serves every provider.

The one keyless built-in is local: it counts as configured when DEV3D_LOCAL_BASE_URL is explicitly set, which is what lets you point the office at an Ollama, vLLM or LM Studio instance with no credential at all. A plugin can declare the same thing with "keyless": true.

Workspaces

Three variables describe where work happens.

  • DEV3D_WORKSPACE is the default project: the directory a run uses when it names no floor, and what the default organisation is created pointing at. A run's workspaceRoot is the root that file tools resolve paths against, so this is not merely a label.
  • DEV3D_WORKSPACES_ROOT is where new project folders are created. When you create a project by name, the name is resolved under this root, which is what keeps "create a project" from being a way to hand employees any directory on the disk.
  • DEV3D_ALLOW_EXTERNAL_WORKSPACES governs the escape hatch: a floor may point at an absolute path outside the root. It is on by default because working on projects that already exist elsewhere is the point of the office, and the operator typing that path is the consent. Only the literal false disables it — 0, no and off all leave it enabled, which is the opposite of what most people expect from a boolean-looking variable.

An external workspace becomes readable and writable to that floor's employees. Turning the boundary off is a real grant, not a preference. It is also one of the eight values copied into installation settings on first boot, so flipping it in .env after the first run does nothing — change it on the Settings page instead.

Plugins

Two directories are scanned at boot, and the label attached to a plugin comes from which one it was found in.

DirectorySource labelWhat belongs there
DEV3D_PLUGINS_DIR (./plugins)bundledPlugins that ship with the checkout, plus anything you drop in by hand. Bundled plugins can be disabled but never deleted through the API: the refusal reads This plugin ships with the office in DEV3D_PLUGINS_DIR. Disable it, or remove the directory yourself.
DEV3D_PLUGIN_INSTALL_DIR (./data/plugins)marketplacePlugins downloaded from a registered marketplace. These are the only ones an upgrade may replace, and the only ones DELETE /api/plugins/:id will remove.

DEV3D_ALLOW_PLUGIN_INSTALL is the gate on the whole install path. It defaults to false, and until it is the literal true the host refuses before it fetches anything: Installing plugins is disabled. Set DEV3D_ALLOW_PLUGIN_INSTALL=true to allow running code downloaded from a marketplace. That wording is deliberate — installing unpacks a .tar.gz and, if the manifest declares an entry, runs someone else's code inside the orchestrator's own process. Registering a marketplace to browse is not gated; only installing is.

The gate is environment-only. It is not in installation settings, so it cannot be flipped from the console, and that is the point: the decision to run downloaded code has to be made in the environment the process was launched from.

Model discovery

Discovery is on by default, and the reason is a concrete failure rather than a preference for freshness: a static list is wrong the moment a vendor ships or retires a model, and this checkout shipped a DeepSeek model that the DeepSeek endpoint did not serve. Asking each configured provider for its own /models list is the only thing that knows.

The division of labour is worth understanding before you debug it. Membership comes from the provider; tier, price, quality and capabilities come from the curated catalogue the server ships. So discovery can add a model nobody has described — it stays routable, flagged as unrated — and it can withdraw a model the provider no longer serves, which is better than leaving it in the list to fail a turn. What discovery cannot do is invent a price or a tier.

Three switches control it. DEV3D_MODEL_DISCOVERY=false keeps the office entirely offline, at which point the curated table stands alone exactly as an old hardcoded catalogue did. DEV3D_MODEL_DISCOVERY_TTL_MS (six hours by default) decides when a known list counts as stale, and 0 means every ask re-asks. DEV3D_MODEL_DISCOVERY_CACHE names the file the results survive a restart in, and the literal off or none disables the cache — which costs a network round trip per provider on every boot, so it is the option for a locked-down install that should not write a file it does not have to.

Discovery never blocks boot. The server starts listening first and asks afterwards, with the cached or curated catalogue in place. A provider that hangs cannot hold the process open, and when the answers arrive every connected console is handed a corrected office.updated. The boot line reports the state: model discovery: on (cache ./data/model-discovery.json, ttl 360m).

Pooled quality and benchmarks

The router weighs three sources of opinion about a model, and only the third of them ever leaves the machine.

SourceWhere it comes fromCost
curatedThe catalogue table that ships with the server, plus any correction an operator makes on the Settings page. An operator override carries confidence 1, above every automatic source, because a human who has run the model on their own work outranks a benchmark that measured somebody else's.None
learnedWhat this office observed from its own finished turns. A done turn with no error is a success; a cancelled turn votes on nothing; and a turn whose servedBy differs from its routed model is recorded as a failure for the routed model and a success for whatever actually answered.None
pooledPublic benchmark indices, from either OpenRouter's benchmark aggregation or Artificial Analysis directly. Confidence 0.55: believed, but less than an operator and no more than the office's own observations.One outbound request, and only with a key

Pooled quality is off unless you authorise it. DEV3D_POOLED_QUALITY_KEY_VAR names the variable holding the Artificial Analysis key, and when that variable is empty refresh() returns no Artificial Analysis API key is configured without making a request at all — a default install makes no outbound call it did not have to. DEV3D_BENCHMARKS is the preferred source when OPENROUTER_API_KEY is set, because OpenRouter's payload returns the Artificial Analysis indices as one of its three sources alongside Design Arena Elo and its own measured runs: one credential covers more ground than two.

Why scores are percentiles

Benchmark scores are ranked as a percentile of the benchmarked population, not mapped through a published window. This matters because the published scale is misleading: measured against the live payload, the median intelligence index is 22.3 with a maximum of 53.4. Mapping that through a scale centred on values around 60–90 would put the median model at 0.11 and never award a top score to anything. A percentile is self-calibrating, and it is honest about being a standing among measured models rather than an absolute statement of capability.

Coverage is partial, and the console says how partial

Coverage is genuinely incomplete rather than nearly complete. The payload uses dated snapshots that often do not match a current model id, so most models a router can pick carry no pooled opinion at all. That is precisely why the learned layer — built from this office's own turns — is usually the one that decides. The Models tab reports the real numbers as measured, matched and total, and the attribution attached to every pooled score names the source and the population it was ranked against, because the source's terms require it.

Endpoint health

Endpoint health is uptime tracking, and it is important to be exact about that because the underlying API is more expressive than the data it returns. The public endpoint list reports three fields, and the measured coverage across 34 endpoints is: uptime_last_30m populated 79% of the time, latency_last_30m 0%, and throughput_last_30m 0%. Latency and throughput are documented fields that are simply empty in the live payload, so a speed-aware router built on them would have been built on zeros. The reader will pick them up if they ever appear; nothing depends on them.

What is tracked is the best uptime across a model's upstream endpoints, not the average. The question the router needs answered is "will a call to this model succeed", and OpenRouter routes around a sick endpoint, so the best endpoint is the right answer to that question. The count of healthy endpoints is kept beside it, because "the only one of twelve still standing" is a fragile position that an average would hide and a count makes visible.

The score consequence is a demotion and never an exclusion. A model at 0% uptime loses 0.30 of score — the same weight class as the tier-affinity term — so it can still win when nothing else fits. Hard-excluding on uptime would be wrong twice over: the figure is a rolling 30-minute reading that can be stale, and removing an option with no way back is a bad failure mode for something the router cannot re-measure on the spot. A model whose uptime is unknown contributes exactly nothing, so nothing is penalised for being unmeasured — otherwise every provider outside OpenRouter would lose to every provider inside it.

DEV3D_ENDPOINT_HEALTH_PROVIDERS is the scope control, and its default of openrouter is the only honest setting out of the box: only models that are OpenRouter slugs underneath can be asked about, because a direct DeepSeek or Anthropic key does not route through OpenRouter and its endpoint health says nothing about the call this office will make.

Lookups are on demand and never block a turn. The router asks for a model's health as it considers it, the answer comes from cache if it is fresh, and a background fetch is scheduled otherwise and returned from immediately. A model nobody routes to is never fetched — which is what keeps one-request-per-model from meaning 445 requests. The same property explains the first-turn behaviour: an unknown model contributes nothing, so the first turn on a new model routes exactly as it would have before health tracking existed, and later turns can see what was learned.

Budget and limits

DEV3D_RUN_BUDGET_USD is read and never used. It is loaded into config.runBudgetUsd and then referenced nowhere else in the source. Setting it changes nothing at all. A run's real ceiling is input.budgetUsd ?? project.budget.defaultRunUsd — the per-submission override if one was given, otherwise the floor's budget.defaultRunUsd, which itself defaults to 5. That coincidence of defaults is the only reason this has gone unnoticed: DEV3D_RUN_BUDGET_USD=5 and the floor's 5 agree, so the variable appears to work. Change it to 25 and nothing happens; change the floor's budget in the console and the ceiling moves.

The rest of the limit story is real and worth knowing in detail, because two of the three mechanisms have holes.

MechanismWhen it actsWhat happens
Hard ceilingChecked before each stage, on >=The whole run fails, with status failed and the error Run budget of $X was exhausted before stage "Y". It also sets abortReason, which only debate and review-loop consult between turns — so single and parallel stages can overshoot the ceiling within a stage before the next check catches it.
Soft spend gateOnce per run, between stages, after spend has crossed DEV3D_SOFT_SPEND_APPROVAL_USDA pending approval of kind 'spend' is raised. Approving continues; refusing sets the run to cancelled — not paused, and not awaiting-approval — with the error The operator declined to continue spending on this run. 0 disables it.
Approval timeoutDEV3D_APPROVAL_TIMEOUT_MS after any approval is raisedCounts as refused, including the soft-spend approval above. There is no way to leave an approval pending forever.

Concurrency is the other limit: DEV3D_MAX_CONCURRENCY defaults to 4 and clamps to 1..16. It is used in exactly three places — the parallel branches of a parallel stage, and the reviewers and the revisions of a review-loop — all through the same bounded map. Nothing else in the engine runs concurrently, so raising it does not make a single stage faster.

Precedence: what wins, and when

Exactly eight engine-affecting values are copied out of the environment into installation settings on a first boot:

Environment variableSetting it becomes
DEV3D_WORKSPACES_ROOTworkspacesRoot
DEV3D_ALLOW_EXTERNAL_WORKSPACESallowExternalWorkspaces
DEV3D_ROUTINGdefaultRoutingPosture
DEV3D_MAX_CONCURRENCYmaxConcurrency
DEV3D_SOFT_SPEND_APPROVAL_USDsoftSpendApprovalUsd
DEV3D_AUTO_APPROVE_SHELLautoApproveShell
DEV3D_APPROVAL_TIMEOUT_MSapprovalTimeoutMs
DEV3D_LOG_LEVELlogLevel

After that first boot, those eight are read from the database rather than the environment, and the console is how they change. On every boot the stored settings are spread over the environment-derived defaults, which means a value that was never saved still tracks the environment and one that has been saved does not. Note the asymmetry in the routing case: the setting is called defaultRoutingPosture because it is the posture new floors start with — a given floor carries its own posture, and the engine reads the floor's value when it has one.

Everything else stays environment-only for the life of the process, on purpose:

  • Provider keys and base URLs. Secrets belong in the environment, and a settings document in a database is a worse place for them.
  • DEV3D_DB and the cache paths. They have to be known before the database can be opened, so they cannot be stored in it.
  • The plugin install gate. The decision to run code downloaded from a marketplace is a launch-time decision.
  • DEV3D_SKILLS_DIR, both plugin directories, and every DEV3D_*_CACHE and discovery path. These are filesystem layout, and they are read once at boot.

HOST and PORT fall in the same category for a different reason: the process needs them before it has anything to save settings into.

Common misconfigurations

SymptomCauseFix
A round bill you did not expect. The office was on scripted employees yesterday.DEV3D_LLM_MODE=auto goes live the instant any provider key appears, and it resolves at boot.Set DEV3D_LLM_MODE=mock while you are looking around. To stop spending without editing anything, set a low softSpendApprovalUsd or a low floor budget instead.
A variable looks set but behaves as if it were not.An empty string counts as absent throughout the config reader. DEV3D_LOG_LEVEL= is the same as not setting it.Give it a value, or remove the line. There is no "set to nothing" state.
A numeric variable has no effect.A non-finite value silently falls back to the default. DEV3D_MAX_CONCURRENCY=lots is 4, and there is no warning.Use a plain number. Remember that some have floors or clamps: concurrency is clamped to 1..16 and the approval timeout has a 1000 ms floor.
You changed .env and nothing happened, even after a restart.A real environment variable beats the file, and the file is only read for keys the environment does not already define.Check what the launching shell or service actually exports. configStaleDetail on /api/health will tell you the file changed; it will not tell you something else won.
You changed .env and nothing happened, with no restart.The environment is read once, at boot. There is no watcher.Restart. The stale badge exists precisely because this is invisible.
A setting you edited in .env after the first run has no effect.It is one of the eight values copied into installation settings on first boot, and saved settings win from then on.Change it on the Settings page instead.
DEV3D_ALLOW_EXTERNAL_WORKSPACES=0 did not close the boundary.Only the literal false disables it. Everything else, including 0 and off, leaves it on.Write false. The same literal-only rule applies to DEV3D_AUTO_APPROVE_SHELL, DEV3D_ALLOW_PLUGIN_INSTALL, DEV3D_MODEL_DISCOVERY=false, DEV3D_ENDPOINT_HEALTH=false and DEV3D_BENCHMARKS=false.
You lowered DEV3D_RUN_BUDGET_USD and runs still cost more.The variable is read and never consumed. The real ceiling is the floor's budget.defaultRunUsd.Set the floor's budget — the default is 5, which is why the variable appears to work at its default.
You pointed a cache at off and the file is still there.off disables reading and writing; it does not delete an existing file.Delete it yourself. The stale file is simply ignored.
A plugin's provider never gets routed to.The provider is catalogued but not configured, so its models are not candidates at all.Set the key variable the manifest named, and restart. The providers endpoint says whether it counts as configured.
Models are missing or stale on a machine with no network.Discovery is on by default, and the cache file is what carries results between restarts.Keep the cache enabled and commit or preserve it, or set DEV3D_MODEL_DISCOVERY=false and accept the curated catalogue as the whole truth.
Nothing about a plugin checksum or an archive path makes sense.You are looking at the wrong layer: the manifest validator decides what loads; the archive reader decides what unpacks.Bundle problems are reported during install with the offending entry named. Manifest problems are reported per field, joined with ; .

What is not configurable

  • The interval at which a run's spend is checked. It is before each stage, and it is not a setting.
  • Whether a stage failure halts the run. That is a property of the pipeline: only a non-optional stage halts, and the one shipped optional stage is research in product-build.
  • The tool caps. Shell's 30-second default and 120-second maximum, the file-read limits and the directory-listing limits are constants in the tool implementations rather than environment.
  • Which providers exist. Five are built in; more arrive as plugin contributions, not as environment variables.

Linked from

Did this page answer your question?