JuliusBrussee/caveman · error

cave_claude_header_invalid

cave_claude_header_invalid

Error message

cave_claude_header_invalid

What it means

mergeClaudeHeader validates header names and values before merging into the Claude headers blob: a name containing ':', CR, or LF, or a value containing CR/LF, throws cave_claude_header_invalid to prevent header injection/malformed headers.

Solutions

  1. Sanitize name/value: strip or reject any of ':', '\r', '\n' before calling
  2. Split 'Name: value' strings into separate name and value arguments
  3. Trim and validate with the same regex the library uses: /[:\r\n]/ on name, /[\r\n]/ on value

Example fix

// before
mergeClaudeHeader(raw, `X-${label}`, valueWithNewline);
// after
const safeName = `X-${label.replace(/[:\r\n]/g, "")}`;
const safeValue = valueWithNewline.replace(/[\r\n]/g, " ");
mergeClaudeHeader(raw, safeName, safeValue);
Defensive patterns

Strategy: validation

Validate before calling

const sanitizeHeader = ([name, value]) => [name.replace(/[:\r\n]/g, ""), value.replace(/[\r\n]/g, " ")];
Object.fromEntries(Object.entries(headers).map(sanitizeHeader));

Type guard

const isSafeHeader = ([n, v]) => !/[:\r\n]/.test(n) && !["\r", "\n"].some(c => v.includes(c));

Try / catch

try { await runClaudeAgent(opts); } catch (e) { if (e.message === "cave_claude_header_invalid") { opts.headers = sanitizeHeaders(opts.headers); return runClaudeAgent(opts); } throw e; }

Prevention

When it happens

Trigger: Passing a header name with a colon or newline (e.g. "X:Foo" or "X\nFoo"), or a value containing CRLF, into the Claude headers configuration (via claudeHeaders / agent definition headers).

Common situations: Building headers from untrusted/user input without sanitizing; template strings embedding newlines; copy-pasted headers that include "Name: value" as a single value.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/c7a8d91e5ca3fa7a. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/claude-runtime.ts:451

  for (const [name, value] of Object.entries(headers)) {
    raw = mergeClaudeHeader(raw, name, value);
  }
  return raw ?? "";
}

function stripCaveHeaders(raw: string | undefined): string | undefined {
  const kept = (raw ?? "").split(/\r\n|\n|\r/).filter((line) => {
    if (!line.trim()) return false;
    const colon = line.indexOf(":");
    const name = (colon < 0 ? line : line.slice(0, colon)).trim().toLowerCase();
    return !name.startsWith("x-cave-");
  });
  return kept.length === 0 ? undefined : kept.join("\n");
}

function mergeClaudeHeader(raw: string | undefined, name: string, value: string): string {
  if (/[:\r\n]/.test(name) || /[\r\n]/.test(value)) {
    throw new Error("cave_claude_header_invalid");
  }
  const target = name.toLowerCase();
  const kept = (raw ?? "").split(/\r\n|\n|\r/).filter((line) => {
    if (!line.trim()) return false;
    const colon = line.indexOf(":");
    return (colon < 0 ? line : line.slice(0, colon)).trim().toLowerCase() !== target;
  });
  kept.push(`${name}: ${value}`);
  return kept.join("\n");
}

function resolveClaudeModel(definition: AgentDefinition, rootDir: string): string {
  const configured = typeof definition.model === "string"
    ? definition.model
    : process.env.CAVE_MODEL ?? localModel(rootDir) ??
      (process.env.ANTHROPIC_API_KEY ? "anthropic/claude-haiku-4-5" : undefined);
  if (configured === undefined) throw new Error("cave_claude_model_required");
  const separator = configured.indexOf("/");

View on GitHub (pinned to 3ee70a1026)