{"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":7021,"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  // Routing through the local proxy makes ANTHROPIC_BASE_URL non-first-party in\n  // Claude Code's eyes, which silently drops the 1M context / ~600k\n  // auto-compact window to 200k (#865). When the proxy's anthropic upstream is\n  // verifiably api.anthropic.com, assert first-party through Claude Code's own\n  // escape hatch. Never touches a value the user already carries in settings.\n  const assumeFirstParty = env[CLAUDE_ASSUME_FIRST_PARTY_ENV] === undefined\n    && wrapMode(gw) === \"local\" && proxyAnthropicUpstreamIsFirstParty();\n  if (assumeFirstParty) env[CLAUDE_ASSUME_FIRST_PARTY_ENV] = \"1\";\n  // Native routing points every later turn at the same loopback gateway, so it\n  // needs the wrap path's proxy exemption too (#1001). Never clobber a value the","sourceCodeStart":7003,"sourceCodeEnd":7039,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L7003-L7039","documentation":"`claudeNativeMutations()` edits Claude's settings.json to inject Caveman env routing. Before writing, it validates that an existing `env` key is a plain JSON object; if `env` exists but is a non-object (string, number, array, null), the CLI refuses to overwrite it to avoid destroying user data. The user must fix the settings file manually.","triggerScenarios":"Running a native Claude setup/mutation command when the parsed settings document has `env` defined but not a non-null plain object (`typeof settings.env !== \"object\" || settings.env === null || Array.isArray(settings.env)`).","commonSituations":"Hand-edited ~/.claude/settings.json where env was accidentally changed to a string or array; a tool or script wrote env as a flat string; corrupted settings from a failed merge; null env from a template placeholder that was never filled.","solutions":["Open the settings file reported in the message and change `env` to a JSON object (e.g. {} or your key/value map)","Back up the file first: cp settings.json settings.json.bak, then fix and re-run the command","If env holds meaningful data in a wrong shape, convert it to key/value form instead of deleting it","Re-run `caveman setup` after fixing the file"],"exampleFix":"// before: ~/.claude/settings.json\n{ \"env\": \"ANTHROPIC_BASE_URL=https://example.com\" }\n// after\n{ \"env\": { \"ANTHROPIC_BASE_URL\": \"https://example.com\" } }","handlingStrategy":"type-guard","validationCode":"// Before running setup, validate the Claude settings env shape:\nconst s = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));\nif (s.env !== undefined && (typeof s.env !== 'object' || s.env === null || Array.isArray(s.env))) {\n  console.error(`Fix ${settingsPath}: env must be a JSON object`);\n}","typeGuard":"function isValidEnvObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  await cavemanSetup(['claude']);\n} catch (e) {\n  if (String(e.message).endsWith('env must be a JSON object; refusing to overwrite it')) {\n    // fix or back up settings.json manually, then retry\n  }\n  throw e;\n}","preventionTips":["Keep `env` in Claude settings as a flat object of string key/value pairs","Validate settings.json with a JSON schema editor plugin before saving","Back up ~/.claude/settings.json before hand edits or tool-driven migrations","Never hand-write env as a string; use a JSON object"],"tags":["config","json-shape","claude","setup"],"backgroundTag":"config-type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}