JuliusBrussee/caveman · error · Error
cannot safely resolve Qwen's effective OPENAI_API_KEY
Error message
cannot safely resolve Qwen's effective OPENAI_API_KEY
What it means
When wrapping Qwen in managed (routed) mode, the CLI must know whether a usable OPENAI_API_KEY would be in effect so it can render the optional key reference in the temp system settings. qwenEffectiveOpenAIKeyAvailable() returns null when it cannot PROVE the effective value: unreadable/ambiguous settings layers, unresolved excluded-env-vars or environment file paths, unparseable dotfiles, a settings.env that is not an object, non-string env values, or `${env:...}`-style reference/newline-bearing values in no-relaunch/sandbox mode. Rather than guess, applyConfigFileInjection throws this fail-closed error.
Source
Thrown at packages/cli/src/index.ts:8841
if (!getObject(overlay, ["mcp", "servers", "caveman"])) return overlay;
const next = cloneJsonValue(overlay);
const mcp = getObject(next, ["mcp"])!;
const servers = getObject(next, ["mcp", "servers"])!;
delete servers["caveman"];
if (Object.keys(servers).length === 0) delete mcp["servers"];
if (Object.keys(mcp).length === 0) delete next["mcp"];
return next;
}
function applyConfigFileInjection(env: NodeJS.ProcessEnv, agent: AgentProfile, inj: ConfigFileInjection, gw: string, modeGw = gw, mcpMode: McpSurfaceMode = "auto", agentArgs: string[] = []) {
const baseConfig = readBaseConfig(inj);
const mode = wrapMode(modeGw);
const staticOverlay = mode === "managed" && inj.config_overlay.managed !== undefined ? inj.config_overlay.managed : inj.config_overlay.local;
const builder = overlayBuilders[agent.id];
const renderOptions: RenderDeepOptions = {};
if (agent.id === "qwen" && mode === "managed") {
const available = qwenEffectiveOpenAIKeyAvailable();
if (available === null) throw new Error("cannot safely resolve Qwen's effective OPENAI_API_KEY");
renderOptions.optionalOpenAIKeyEnvAvailable = available;
}
let rawOverlay = renderDeep(
builder ? builder(agent, baseConfig, { mode, gatewayUrl: gw, env }) : staticOverlay,
gw,
env,
renderOptions,
);
if (agent.id === "qwen" && mcpMode === "auto") {
const ownedMcp = ownedMcpRegistration(agent.id, agentArgs);
if (ownedMcp) {
// Qwen merges system settings last. Mirror only a still-journaled native
// entry into this temporary system overlay so project settings cannot
// replace the recovery server after Caveman has claimed it is available.
rawOverlay = deepMerge(rawOverlay, { mcpServers: { caveman: qwenMcpEntry(ownedMcp) } });
} else {
// Qwen can advertise MCP schemas even when enterprise policy blocks calls.
// Suppress the exact server in this highest-precedence temporary overlayView on GitHub (pinned to 5184b3d11a)
Solutions
- Set OPENAI_API_KEY explicitly in the process environment (export OPENAI_API_KEY=...) before running the wrap — process env is the first and safest tier.
- Set the key literally (a plain string, no ${env:...} references or newlines) in Qwen's settings.env or home env fallback.
- Repair or remove malformed Qwen settings layers (~/.qwen settings files, project dotfiles) that fail to parse.
- Unset QWEN_CODE_NO_RELAUNCH / SANDBOX if you don't specifically need them, so the normal relaunch resolution path can be used.
- Run `caveman setup`/doctor-style checks to confirm settings files are readable.
Example fix
// before: template reference in settings.env (unsafe to resolve)
OPENAI_API_KEY=${env:OPENAI_API_KEY}
// after: literal value in process env before wrap
export OPENAI_API_KEY="sk-..." && caveman qwen Defensive patterns
Strategy: validation
Validate before calling
// Run before `caveman wrap qwen` in managed mode
const key = process.env.OPENAI_API_KEY;
if (!key || !key.trim()) throw new Error("Set OPENAI_API_KEY in the process env before wrapping qwen in managed mode");
if (/\$\{env:/.test(key) || /[\r\n]/.test(key)) throw new Error("OPENAI_API_KEY must be a literal string (no ${env:...} refs, no newlines)"); Type guard
function isLiteralApiKey(v: unknown): v is string {
return typeof v === "string" && v.trim().length > 0 && !/\$\{env:/.test(v) && ![\r\n].test(v);
} Try / catch
try {
execSync("caveman qwen", { stdio: "inherit", env: { ...process.env, OPENAI_API_KEY: requiredKey } });
} catch (e) {
if (e instanceof Error && e.message.includes("cannot safely resolve Qwen's effective OPENAI_API_KEY")) {
console.error("Export a literal OPENAI_API_KEY and fix malformed ~/.qwen settings files, then retry.");
process.exit(1);
}
throw e;
} Prevention
- Always export a literal OPENAI_API_KEY in the shell/CI environment for managed qwen wraps.
- Never store the key as a ${env:...} template reference in Qwen settings.env.
- Keep ~/.qwen settings files valid JSON and hand-edits minimal.
- Avoid combining QWEN_CODE_NO_RELAUNCH or SANDBOX with key resolution that relies on late-loaded dotfiles.
When it happens
Trigger: Running `caveman wrap qwen` (or the qwen shortcut) against a managed gateway where: Qwen settings layers cannot be read; QWEN_CODE_NO_RELAUNCH or SANDBOX effective values cannot be safely resolved; OPENAI_API_KEY comes only from a workspace/system dotenv that fails to parse; or settings.env contains a non-string or `${env:...}` reference for OPENAI_API_KEY in no-relaunch/sandbox mode.
Common situations: Users keeping OPENAI_API_KEY only in a Qwen project `.env` file that the CLI cannot safely attribute; CI sandboxes where SANDBOX=1 and the key is referenced via a template instead of set literally; corrupted or hand-edited ~/.qwen settings files; unusual env-var quoting with embedded newlines.
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
Related errors
- cannot safely resolve Qwen's effective settings
- CAVEMAN_BEDROCK_ENDPOINT must be runtime or mantle
- managed Bedrock wrap requires a valid CAVE_API_KEY
- cave_harness_model_identity_missing
- caveman build: set CAVE_MODEL when zero or multiple provider
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-09-06).
Data as JSON: /api/errors/34621086bbc9999d.
Report an issue: GitHub.