{"record":{"id":"c7a8d91e5ca3fa7a","repo":"JuliusBrussee/caveman","slug":"cave-claude-header-invalid","errorCode":null,"errorMessage":"cave_claude_header_invalid","messagePattern":"cave_claude_header_invalid","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/claude-runtime.ts","lineNumber":451,"sourceCode":"  for (const [name, value] of Object.entries(headers)) {\n    raw = mergeClaudeHeader(raw, name, value);\n  }\n  return raw ?? \"\";\n}\n\nfunction stripCaveHeaders(raw: string | undefined): string | undefined {\n  const kept = (raw ?? \"\").split(/\\r\\n|\\n|\\r/).filter((line) => {\n    if (!line.trim()) return false;\n    const colon = line.indexOf(\":\");\n    const name = (colon < 0 ? line : line.slice(0, colon)).trim().toLowerCase();\n    return !name.startsWith(\"x-cave-\");\n  });\n  return kept.length === 0 ? undefined : kept.join(\"\\n\");\n}\n\nfunction mergeClaudeHeader(raw: string | undefined, name: string, value: string): string {\n  if (/[:\\r\\n]/.test(name) || /[\\r\\n]/.test(value)) {\n    throw new Error(\"cave_claude_header_invalid\");\n  }\n  const target = name.toLowerCase();\n  const kept = (raw ?? \"\").split(/\\r\\n|\\n|\\r/).filter((line) => {\n    if (!line.trim()) return false;\n    const colon = line.indexOf(\":\");\n    return (colon < 0 ? line : line.slice(0, colon)).trim().toLowerCase() !== target;\n  });\n  kept.push(`${name}: ${value}`);\n  return kept.join(\"\\n\");\n}\n\nfunction resolveClaudeModel(definition: AgentDefinition, rootDir: string): string {\n  const configured = typeof definition.model === \"string\"\n    ? definition.model\n    : process.env.CAVE_MODEL ?? localModel(rootDir) ??\n      (process.env.ANTHROPIC_API_KEY ? \"anthropic/claude-haiku-4-5\" : undefined);\n  if (configured === undefined) throw new Error(\"cave_claude_model_required\");\n  const separator = configured.indexOf(\"/\");","sourceCodeStart":433,"sourceCodeEnd":469,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/claude-runtime.ts#L433-L469","documentation":"mergeClaudeHeader() builds raw HTTP header text that gets injected into the SDK's header configuration, so header names containing ':' or CR/LF, or values containing CR/LF, are rejected outright — they would allow header smuggling or request splitting. Any violation throws cave_claude_header_invalid before the request is made.","triggerScenarios":"Passing a header name via the Claude harness options that includes a colon (e.g. \"X-Cave-Trace: extra\") or any \\r/\\n; or a header value with embedded newlines (multi-line tokens, pasted certs, values read from a file with trailing CRLF that wasn't trimmed).","commonSituations":"Reading header values from .env or config files on Windows (CRLF line endings leak \\r into values); copy-pasting multi-line API keys or bearer tokens; programmatically composing header names from user input without sanitization.","solutions":["Trim and single-line the value: value.replace(/[\\r\\n]+/g, \"\").trim() before passing it in.","Fix the source: strip CRLF when loading config files, or quote env values so the shell doesn't embed newlines.","Validate header names are token-shaped (^[A-Za-z0-9-]+$) at your config layer so a colon can never reach mergeClaudeHeader."],"exampleFix":"// before: value read from a CRLF config file\nconst token = readLine(\"auth.txt\"); // \"abc123\\r\"\nmergeClaudeHeader(raw, \"authorization\", token); // throws\n\n// after: normalize before merging\nconst token = readLine(\"auth.txt\").replace(/[\\r\\n]+/g, \"\").trim();\nmergeClaudeHeader(raw, \"authorization\", token);","handlingStrategy":"validation","validationCode":"function assertHeaderSafe(name: string, value: string): void {\n  if (!/^[!#$%&'*+.^_|~0-9A-Za-z-]+$/.test(name) || /[\\r\\n]/.test(value)) {\n    throw new Error(`unsafe header ${name}`);\n  }\n}\n// run before composing options that reach the Claude harness\nassertHeaderSafe(\"x-cave-trace\", traceValue.replace(/[\\r\\n]+/g, \"\").trim());","typeGuard":"function isSafeHeaderValue(value: string): boolean {\n  return !/[\\r\\n]/.test(value);\n}","tryCatchPattern":"try {\n  merged = mergeClaudeHeader(merged, name, value);\n} catch (error) {\n  if (error instanceof Error && error.message === \"cave_claude_header_invalid\") {\n    // source data is malformed — sanitize or reject the config, never retry as-is\n    throw new Error(`header '${name}' rejected: sanitize CR/LF and ':' before retry`);\n  }\n  throw error;\n}","preventionTips":["Strip CR/LF and trim every header value sourced from files, env, or user input before it reaches the harness options.","Restrict header names to a token regex at your config boundary.","On Windows, normalize CRLF when reading config files that feed header values."],"tags":["headers","security","input-validation","claude-sdk"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}