JuliusBrussee/caveman · error

CAVEMAN_BEDROCK_ENDPOINT must be runtime or mantle

Error message

CAVEMAN_BEDROCK_ENDPOINT must be runtime or mantle

What it means

applyClaudeBedrockWrap validates CAVEMAN_BEDROCK_ENDPOINT when CAVEMAN_WRAP_PROVIDER is "bedrock" for the claude agent. The variable accepts only "runtime" (default) or "mantle"; any other value is rejected because the wrapper can only remove the generic Anthropic route and wire exactly one of these two documented transport lanes.

Solutions

  1. Set CAVEMAN_BEDROCK_ENDPOINT=runtime (or remove it — "runtime" is the default).
  2. Set CAVEMAN_BEDROCK_ENDPOINT=mantle if you specifically want Claude Code's Mantle gateway transport.
  3. Do not put a real AWS endpoint URL in this variable; it is a mode selector, not a URL.
  4. Check casing after trimming/lowercasing is applied — "RUNTIME" is fine, "runtime-fips" is not.

Example fix

// before
export CAVEMAN_BEDROCK_ENDPOINT="https://bedrock-runtime.us-east-1.amazonaws.com"
// after
export CAVEMAN_BEDROCK_ENDPOINT="runtime"
Defensive patterns

Strategy: validation

Validate before calling

const ep = (process.env.CAVEMAN_BEDROCK_ENDPOINT ?? "runtime").trim().toLowerCase();
if (process.env.CAVEMAN_WRAP_PROVIDER?.trim().toLowerCase() === "bedrock" && ep !== "runtime" && ep !== "mantle") {
  throw new Error(`CAVEMAN_BEDROCK_ENDPOINT='${ep}' invalid; use "runtime" or "mantle"`);
}

Type guard

function isBedrockEndpoint(v: string | undefined): v is "runtime" | "mantle" {
  const s = (v ?? "runtime").trim().toLowerCase();
  return s === "runtime" || s === "mantle";
}

Try / catch

try {
  applyClaudeBedrockWrap(env, agent, renderedGw, modeGw);
} catch (e) {
  if (String((e as Error).message).startsWith("CAVEMAN_BEDROCK_ENDPOINT must be")) {
    // fall back to default: delete the variable and retry with "runtime"
  }
}

Prevention

When it happens

Trigger: Setting CAVEMAN_WRAP_PROVIDER=bedrock and CAVEMAN_BEDROCK_ENDPOINT to something like a full https:// URL, "bedrock-runtime", "eu", or a typo such as "Runtime" beyond trimming/lowercasing is handled, so only genuinely different strings throw.

Common situations: Operators pasting a regional Bedrock endpoint URL into the variable assuming it wants an actual endpoint; typos like "mantle-gw"; copying the variable name from another tool's docs.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at packages/cli/src/index.ts:9276

  if (!accessKey || !secretKey) {
    throw new Error("AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set together");
  }
  return [accessKey, secretKey, ...(sessionToken ? [sessionToken] : [])].join(":");
}

// applyClaudeBedrockWrap selects Claude Code's native Bedrock or opt-in Mantle
// transport only when the operator explicitly requests it. The default Claude
// profile remains Anthropic-wire. Runtime uses Claude Code's documented AWS
// credential chain; unlike Mantle, Claude Code exposes no supported Runtime
// authentication-bypass variable. In managed mode the validated BYOK value also
// rides through Claude's custom headers. Stored-only, server-injected Claude
// Code auth therefore uses Mantle's documented gateway mode.
function applyClaudeBedrockWrap(env: NodeJS.ProcessEnv, agent: AgentProfile, renderedGw: string, modeGw: string): boolean {
  if (agent.id !== "claude" || process.env.CAVEMAN_WRAP_PROVIDER?.trim().toLowerCase() !== "bedrock") return false;

  const endpoint = process.env.CAVEMAN_BEDROCK_ENDPOINT?.trim().toLowerCase() || "runtime";
  if (endpoint !== "runtime" && endpoint !== "mantle") {
    throw new Error("CAVEMAN_BEDROCK_ENDPOINT must be runtime or mantle");
  }

  // Remove the generic/profile Anthropic route and any stale Bedrock selection
  // inherited from the shell. Exactly one Claude Code provider lane is active.
  for (const key of WRAP_BASE_URL_ENV_VARS) delete env[key];
  delete env.ANTHROPIC_AUTH_TOKEN;
  delete env.CLAUDE_CODE_USE_BEDROCK;
  // Remove stale values if a caller previously relied on this undocumented
  // variable. We intentionally never set it.
  delete env.CLAUDE_CODE_SKIP_BEDROCK_AUTH;
  delete env.ANTHROPIC_BEDROCK_BASE_URL;
  delete env.CLAUDE_CODE_USE_MANTLE;
  delete env.CLAUDE_CODE_SKIP_MANTLE_AUTH;
  delete env.ANTHROPIC_BEDROCK_MANTLE_BASE_URL;

  const bedrockBase = appendUrlPath(renderedGw, "/bedrock");
  if (endpoint === "mantle") {
    env.CLAUDE_CODE_USE_MANTLE = "1";

View on GitHub (pinned to 3ee70a1026)