ErrLookup › Background articles › "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them
"environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them
"environment variable is not set", "Missing keys=['...'] in environment.", "<VAR> is required for ..." are the messages of the missing-env-var family: errors a library or CLI raises when a required environment variable is absent, empty, or whitespace-only at the moment the code reads it. A developer meets this family when a process starts or a client is constructed under Docker, systemd, cron, CI, or a GUI launch that did not inherit an exported variable, when only part of a required group is configured, or when a value is set but empty, mis-spelled, or read at the wrong time. ErrLookup documents 166 records of this family across 28 repositories, from LiteLLM telemetry callbacks and AnythingLLM provider classes to deletion CLIs that refuse to run without full configuration.
Distilled from 166 documented records across 28 repositories.
Background
These errors come from the library's own configuration validation, not from the operating system: the environment itself works, but the code's expectation that a named variable holds a usable value failed. The check runs in several places across the family: constructors (LiteLLM's OpenMeter, Arize, PostHog and Levo callbacks validate in __init__; AnythingLLM and Chroma provider classes throw in constructors), CLI preflight (buzz-deletion's required_env() refuses to start, buzz-admin checks BUZZ_RELAY_PRIVATE_KEY before member commands, the ECC ito wrapper checks ECC_ITO_CLI_EXECUTABLE before spawning anything), factory and lazy resolution (nautilus_trader's Credential::resolve, CodeWhale's FleetAlertSecretResolver at dispatch, GitButler's OpenAiProvider when client() is first built), and endpoint guards (LiteLLM's STORE_MODEL_IN_DB gate returns HTTP 500). The shared design intent is fail fast: report misconfiguration before any work starts.
From the caller's side the family varies along two axes: when validation runs and how severe it is. Constructor-time checks crash at startup or first import, so adding 'openmeter' to a callbacks list without OPENMETER_API_KEY breaks callback initialization, and constructing a Turso Database with remoteWritesExperimental and a lazy url provider that returns null throws at open time. Per-request checks surface as failed requests instead of startup crashes: AnythingLLM instantiates a provider class on selection, so a missing FIREWORKS_AI_LLM_API_KEY or ANTHROPIC_API_KEY fails the first chat or embedding request. Some members are warnings that proceed: turbo prints "finished with warnings" and lists each missing variable with its task id while the run completes, and claude-mem skips server key bootstrap with a warning, falling back to the worker path. Nearly every message names the offending variable, which is the single most useful property of the family.
What counts as "missing" is library-specific. LiteLLM's PostHog check tests only for None, so an empty string passes init and fails later at send time; buzz-deletion and ECC trim values and reject whitespace-only ones; caveman also rejects keys containing line breaks because the value is delivered as an HTTP header and a newline would enable header injection. Fallback chains complicate diagnosis: Chroma accepts an api_key argument or an environment variable and auto-detects vendor names like CLOUDFLARE_API_KEY or COHERE_API_KEY over the CHROMA_* defaults, LiteLLM's salt key falls back to the master key, and GitButler tries its secret store before OPENAI_API_KEY in the environment. Groups and pairs fail when half-configured: nautilus_trader resolves credentials only when a complete AX_API_KEY/AX_API_SECRET pair exists, and the Levo callback requires all four LEVOAI_* values.
The dominant real-world trigger is environment context mismatch: a variable exported in an interactive shell is invisible to systemd, launchd, cron, Docker containers, CI jobs, and GUI-launched desktop apps, a point the GitButler, CodeWhale, buzz-admin and LiteLLM records all make explicitly. Name resolution adds its own traps: Windows environment lookups are case-insensitive while env::vars returns stored casing, so rustfs's prefix scan misses a lowercase rustfs_policy_plugin_url, and AnythingLLM matches LLM_PROVIDER values case-sensitively in a switch. Some tools refuse to guess on purpose: buzz-deletion ships no localhost defaults because it hard-deletes data across PostgreSQL, S3 and Redis, and buzz-admin will not generate an ephemeral signing key because clients verify the relay's known pubkey.
Common causes
- Right variable, wrong process context. The variable is exported in an interactive shell, but the process that reads it runs under Docker, systemd, launchd, cron, CI, or a desktop launch that never inherited it. The records repeatedly direct operators to verify inside the exact runtime (printenv in the container, docker exec, Environment= in the unit) rather than in their login shell.
- Variable simply not configured. The deployment never sets it: operators commonly export only DATABASE_URL for buzz-deletion, which also requires REDIS_URL and the BUZZ_S3_* group, or enable an observability callback without its API key. Partial copies of a group, such as some of the four LEVOAI_* values from the Levo dashboard, hit the same path.
- Set but empty or whitespace-only. buzz-deletion trims values and treats whitespace-only entries (quoted empties, trailing spaces in .env files) as missing; ECC trims before its check; caveman also rejects values containing \r or \n, which arrive from password managers or CI block scalars, because the key becomes an HTTP header.
- Wrong name, case, or spelling. FIREWORKS_AI_LLM_API_KEY is not FIREWORKS_API_KEY; AnythingLLM matches LLM_PROVIDER case-sensitively, so 'open_ai' and 'OpenAI' fail; on Windows a lowercase rustfs_policy_plugin_url escapes a case-sensitive prefix scan even though env lookups themselves are case-insensitive; LiteLLM 'os.environ/NAME' webhook references fail on spelling mismatches with the exported name.
- Half-configured pair or group. nautilus_trader assembles credentials from config plus environment and fails when only one of AX_API_KEY/AX_API_SECRET exists; AnythingLLM's TTS_OPEN_AI_COMPATIBLE_ENDPOINT is often set under the STT_ prefix instead, which does not satisfy the TTS constructor. Mixed sources (config overriding env) make the pair diverge.
- Value read at the wrong time. Lazy providers evaluate at construction: Turso invokes the url function immediately when remoteWritesExperimental is enabled and throws on null, while the plain sync engine tolerates it. .env files load at boot, so adding a variable requires a full process restart, and the rustfs and nautilus_trader records both note environments that changed between two reads.
- Fallback chain exhausted. GitButler falls back from its secret store to OPENAI_API_KEY, LiteLLM's salt key falls back to the master key, and Chroma tries an api_key argument before environment variables. When every link is absent the error names the last one, hiding the chain that led there.
- Missing opt-in flag or declaration. LiteLLM's cache-settings and cost-discount endpoints are gated behind STORE_MODEL_IN_DB=True and return HTTP 500 without it; turbo's strict env mode warns for variables declared in turbo.json env/globalEnv/passThroughEnv but unset at runtime; claude-mem non-interactive installs skip the API key prompt and leave a keyless provider config behind.
What usually fixes it
- Set the exact variable, with the exact case and a non-empty trimmed value, in the environment of the process that actually reads it. Verify from inside that context (printenv in the container, docker exec, the unit file's Environment=) rather than from your login shell, then restart or recreate the process.
- Fail fast: assert required variables in a startup preflight or CI step before the library does, so misconfiguration stops the deploy with one report naming every missing key instead of failing mid-request or mid-command. Diff declared requirements (turbo.json env blocks, callback lists) against what the environment actually provides.
- Keep complete, per-tool env templates: all members of a group from one source (all four LEVOAI_* values, both halves of a credential pair, the TTS_ and STT_ twins), and remember values load at boot, so a restart is part of the fix.
- Treat empty as missing: trim whitespace, avoid quoted empties in .env files, and keep secrets single-line (no CI block scalars, strip trailing newlines) especially where the value feeds an HTTP header.
- Where a tool offers degradation, use it deliberately: enable features conditionally on the variable being present (remoteWritesExperimental only when the URL resolves), run turbo with --env-mode=loose for genuinely optional variables, and leave optional integrations unset (rustfs logs "OPA is not enabled." and uses defaults when RUSTFS_POLICY_PLUGIN_URL is absent).
- Prefer the library's machine-readable signals over string matching on the message: error kind() or type discriminants such as OpaConfigError::kind() == "missing_required_env", warning blocks that name the variable and task id, and documented fallback paths like claude-mem's worker mode.
Documented occurrences
- Missing required env var: {0} (rustfs/rustfs)
- Cannot migrate covered tables: no salt key / master key is set. Set LITELLM_SALT_KEY before migrating. (BerriAI/litellm)
- remoteWritesExperimental requires a non-null URL (tursodatabase/turso)
- LEVOAI_ORG_ID environment variable is required for Levo integration. (BerriAI/litellm)
- Invalid webhook url value for: {webhook_urls}. Got type={type(_env_value)} (BerriAI/litellm)
- Provider=${options.provider} requested non-interactively. API key prompt skipped — set CLAUDE_MEM_${options.provider.toUpperCase()}_API_KEY and CLAUDE_MEM_PROVIDER in settings.json or env manually if not already set. (thedotmack/claude-mem)
- Missing keys={missing_keys} in environment. (BerriAI/litellm)
- API credentials not configured (nautechsystems/nautilus_trader)
- No valid endpoint found for Arize, please set 'ARIZE_ENDPOINT' to your GRPC endpoint or 'ARIZE_HTTP_ENDPOINT' to your HTTP endpoint (BerriAI/litellm)
- Invalid message format (gitbutlerapp/gitbutler)
- BUZZ_RELAY_PRIVATE_KEY is required for add-member/remove-member. The relay must have a stable signing key to publish kind:13534 events. (block/buzz)
- Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature. (BerriAI/litellm)
- No OpenAI compatible endpoint was set. Please set this to use your OpenAI compatible TTS service. (Mintplex-Labs/anything-llm)
- POSTHOG_API_KEY is not set, set 'POSTHOG_API_KEY=<>' (BerriAI/litellm)
- finished with warnings (vercel/turborepo)
- No FireworksAI API key was set. (Mintplex-Labs/anything-llm)
- fleet alert secret {name} is not configured (Hmbown/CodeWhale)
- remoteWritesExperimental requires a non-null URL (tursodatabase/turso)
- The canonical ito-compute-cli is unpublished and ECC will not resolve a credential-bearing "ito" executable from PATH. Build it from ${CANONICAL_REPOSITORY.replace(/\.git$/, "")}/${CANONICAL_PACKAGE_PATH}, run npm ci and npm run check, then set ${EXECUTABLE_OVERRIDE} to the explicit absolute dist/bin/ito.js path. (affaan-m/ECC)
- managed Bedrock wrap requires a valid CAVE_API_KEY (JuliusBrussee/caveman)
…and 146 more across the corpus — use search.
Honest provenance: generated on 2026-08-19 from AI-assisted analysis of the linked records. See how records are made.