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

  1. Open ~/.claude/settings.json and convert `env` to a JSON object of key/value strings (or delete the env key so Caveman creates it)
  2. Validate the file parses as JSON (jq ~/.claude/settings.json) before retrying
  3. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/f72d5a2ae2c695ac. Report an issue: GitHub.