{"record":{"id":"f72d5a2ae2c695ac","repo":"JuliusBrussee/caveman","slug":"settingspath-env-must-be-a-json-object-refusin","errorCode":null,"errorMessage":"${settingsPath} env must be a JSON object; refusing to overwrite it","messagePattern":"(.+?) env must be a JSON object; refusing to overwrite it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":6040,"sourceCode":"    const invocation = portableInvocation(binary, [\"--version\"]);\n    const out = spawnSync(invocation.command, invocation.args, { encoding: \"utf8\", timeout: 3000 });\n    if (out.error) return { binary, launchable: false, version: null, error: boundedHookString(out.error.message, 240) ?? \"version_probe_failed\" };\n    const value = `${out.stdout ?? \"\"} ${out.stderr ?? \"\"}`.trim();\n    if (out.status !== 0) return { binary, launchable: false, version: value ? value.slice(0, 160) : null, error: `version_probe_exit_${out.status ?? \"unknown\"}` };\n    return { binary, launchable: true, version: value ? value.slice(0, 160) : null, error: null };\n  } catch {\n    return { binary, launchable: false, version: null, error: \"version_probe_failed\" };\n  }\n}\n\nfunction detectedAgentVersion(agent: AgentProfile): string | null { return nativeHostProbe(agent).version; }\n\nfunction claudeNativeMutations(gw: string, mcpBinary: string): NativeMutation[] {\n  const settingsPath = claudeSettingsPath();\n  const settingsBefore = fileBytes(settingsPath);\n  const settings = parseJsonFileObject(settingsPath, settingsBefore);\n  if (settings.env !== undefined && (typeof settings.env !== \"object\" || settings.env === null || Array.isArray(settings.env))) {\n    throw new Error(`${settingsPath} env must be a JSON object; refusing to overwrite it`);\n  }\n  assertNativeHooksShape(settingsPath, settings, \"claude\");\n  const env = settings.env && typeof settings.env === \"object\" && !Array.isArray(settings.env)\n    ? settings.env as Record<string, unknown>\n    : {};\n  const route = appendUrlPath(gw, \"/w/claude\");\n  const previousRoute = env.ANTHROPIC_BASE_URL;\n  env.ANTHROPIC_BASE_URL = route;\n  settings.env = env;\n  const withHooks = nativeHooksDocument(\"claude\", true, settings);\n\n  const mcpPath = join(homedir(), \".claude.json\");\n  const mcpBefore = fileBytes(mcpPath);\n  const mcpRoot = parseJsonFileObject(mcpPath, mcpBefore);\n  if (mcpRoot.mcpServers !== undefined && (typeof mcpRoot.mcpServers !== \"object\" || mcpRoot.mcpServers === null || Array.isArray(mcpRoot.mcpServers))) {\n    throw new Error(`${mcpPath} mcpServers must be a JSON object; refusing to overwrite it`);\n  }\n  const servers = mcpRoot.mcpServers && typeof mcpRoot.mcpServers === \"object\" && !Array.isArray(mcpRoot.mcpServers)","sourceCodeStart":6022,"sourceCodeEnd":6058,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/cli/src/index.ts#L6022-L6058","documentation":"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.","triggerScenarios":"Enabling native Claude routing while ~/.claude/settings.json contains a non-object `env` key, e.g. \"env\": \"...\" , \"env\": [\"K=V\"], or \"env\": null.","commonSituations":"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.","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"],"exampleFix":"// before — ~/.claude/settings.json\n{ \"env\": [\"ANTHROPIC_BASE_URL=https://example.com\"] }\n\n// after\n{ \"env\": { \"ANTHROPIC_BASE_URL\": \"https://example.com\" } }","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nfunction claudeEnvShapeOk(path: string): boolean {\n  try {\n    const env = JSON.parse(readFileSync(path, \"utf8\")).env;\n    return env === undefined || (typeof env === \"object\" && env !== null && !Array.isArray(env));\n  } catch { return true; /* parseJsonFileObject handles invalid JSON separately */ }\n}\nif (!claudeEnvShapeOk(`${homedir}/.claude/settings.json`)) fixSettingsEnvFirst();","typeGuard":"const isJsonObject = (v: unknown): v is Record<string, unknown> =>\n  typeof v === \"object\" && v !== null && !Array.isArray(v);","tryCatchPattern":"try { nativeInstallClaude(); } catch (e) {\n  if (e instanceof Error && /env must be a JSON object/.test(e.message)) {\n    fixClaudeSettingsEnvShape(); nativeInstallClaude();\n  } else throw e;\n}","preventionTips":["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"],"tags":["config","json","claude","shape-validation","refuse-overwrite"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}