JuliusBrussee/caveman · error
${settingsPath} env must be a JSON object; refusing to overw
Error message
${settingsPath} env must be a JSON object; refusing to overwrite it What it means
claudeNativeMutations() reads ~/.claude/settings.json (via claudeSettingsPath) before writing the ANTHROPIC_BASE_URL route. Claude stores environment overrides in an `env` key that must be a JSON object; if `env` exists but is a string, number, array, or null, Caveman refuses to merge into it rather than risk destroying user data.
Source
Thrown at packages/cli/src/index.ts:6040
const invocation = portableInvocation(binary, ["--version"]);
const out = spawnSync(invocation.command, invocation.args, { encoding: "utf8", timeout: 3000 });
if (out.error) return { binary, launchable: false, version: null, error: boundedHookString(out.error.message, 240) ?? "version_probe_failed" };
const value = `${out.stdout ?? ""} ${out.stderr ?? ""}`.trim();
if (out.status !== 0) return { binary, launchable: false, version: value ? value.slice(0, 160) : null, error: `version_probe_exit_${out.status ?? "unknown"}` };
return { binary, launchable: true, version: value ? value.slice(0, 160) : null, error: null };
} catch {
return { binary, launchable: false, version: null, error: "version_probe_failed" };
}
}
function detectedAgentVersion(agent: AgentProfile): string | null { return nativeHostProbe(agent).version; }
function claudeNativeMutations(gw: string, mcpBinary: string): NativeMutation[] {
const settingsPath = claudeSettingsPath();
const settingsBefore = fileBytes(settingsPath);
const settings = parseJsonFileObject(settingsPath, settingsBefore);
if (settings.env !== undefined && (typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env))) {
throw new Error(`${settingsPath} env must be a JSON object; refusing to overwrite it`);
}
assertNativeHooksShape(settingsPath, settings, "claude");
const env = settings.env && typeof settings.env === "object" && !Array.isArray(settings.env)
? settings.env as Record<string, unknown>
: {};
const route = appendUrlPath(gw, "/w/claude");
const previousRoute = env.ANTHROPIC_BASE_URL;
env.ANTHROPIC_BASE_URL = route;
settings.env = env;
const withHooks = nativeHooksDocument("claude", true, settings);
const mcpPath = join(homedir(), ".claude.json");
const mcpBefore = fileBytes(mcpPath);
const mcpRoot = parseJsonFileObject(mcpPath, mcpBefore);
if (mcpRoot.mcpServers !== undefined && (typeof mcpRoot.mcpServers !== "object" || mcpRoot.mcpServers === null || Array.isArray(mcpRoot.mcpServers))) {
throw new Error(`${mcpPath} mcpServers must be a JSON object; refusing to overwrite it`);
}
const servers = mcpRoot.mcpServers && typeof mcpRoot.mcpServers === "object" && !Array.isArray(mcpRoot.mcpServers)View on GitHub (pinned to 27d5a3981a)
Solutions
- Open ~/.claude/settings.json and convert `env` to a JSON object of key/value strings (or delete the env key so Caveman creates it)
- Validate the file parses as JSON (jq ~/.claude/settings.json) before retrying
- Re-run the native install command for claude
Example fix
// before — ~/.claude/settings.json
{ "env": ["ANTHROPIC_BASE_URL=https://example.com"] }
// after
{ "env": { "ANTHROPIC_BASE_URL": "https://example.com" } } Defensive patterns
Strategy: validation
Validate before calling
import { readFileSync } from "node:fs";
function claudeEnvShapeOk(path: string): boolean {
try {
const env = JSON.parse(readFileSync(path, "utf8")).env;
return env === undefined || (typeof env === "object" && env !== null && !Array.isArray(env));
} catch { return true; /* parseJsonFileObject handles invalid JSON separately */ }
}
if (!claudeEnvShapeOk(`${homedir}/.claude/settings.json`)) fixSettingsEnvFirst(); Type guard
const isJsonObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
Try / catch
try { nativeInstallClaude(); } catch (e) {
if (e instanceof Error && /env must be a JSON object/.test(e.message)) {
fixClaudeSettingsEnvShape(); nativeInstallClaude();
} else throw e;
} Prevention
- Never store env overrides as an array of KEY=VALUE strings in Claude settings.json; always use an object
- Back up agent settings files before running integration installers
- After any hand-edit, validate with jq: jq -e '.env | type == "object" or .env == null' ~/.claude/settings.json
When it happens
Trigger: Enabling native Claude routing while ~/.claude/settings.json contains a non-object `env` key, e.g. "env": "..." , "env": ["K=V"], or "env": null.
Common situations: Hand-edited settings.json with env as an array of "KEY=VALUE" strings (common shell-style habit); a corrupted or truncated settings.json from a crash mid-write; another tool writing env as a plain string.
Related errors
- ${mcpPath} mcpServers must be a JSON object; refusing to ove
- ${settingsPath} mcpServers must be a JSON object; refusing t
- ${configPath} provider must be a JSON object; refusing to ov
- ${configPath} mcp must be a JSON object; refusing to overwri
- caveman build: invalid .caveman/provider.json
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/f72d5a2ae2c695ac.
Report an issue: GitHub.