JuliusBrussee/caveman · error

AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set…

Error message

AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set together

What it means

When no Bedrock bearer token is present, the wrapper falls back to static AWS keys. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are only meaningful together; if exactly one of them (or only a session token) is set, the credential triple cannot be constructed, so it throws. AWS_SESSION_TOKEN alone is also insufficient.

Solutions

  1. Export both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY together (plus AWS_SESSION_TOKEN if using temporary credentials).
  2. Alternatively set AWS_BEARER_TOKEN_BEDROCK to skip the key-based path entirely.
  3. Audit your CI secret list / .env to confirm the key pair was not truncated or partially defined.
  4. If you have no Bedrock credentials, unset all three variables so the function cleanly returns undefined.

Example fix

// before (shell)
export AWS_SESSION_TOKEN="FwoGZX..."
// after
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="wJalr..."
export AWS_SESSION_TOKEN="FwoGZX..."
Defensive patterns

Strategy: validation

Validate before calling

const has = (k: string) => typeof process.env[k] === "string" && process.env[k]!.trim() !== "";
if (!has("AWS_BEARER_TOKEN_BEDROCK")) {
  const a = has("AWS_ACCESS_KEY_ID"), s = has("AWS_SECRET_ACCESS_KEY");
  if (a !== s) throw new Error("Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY together (or neither, using a bearer token)");
}

Try / catch

try {
  applyBedrockWrap(env, agent);
} catch (e) {
  if (String((e as Error).message).includes("must be set together")) {
    // surface which of the pair is missing and abort with instructions
  }
}

Prevention

When it happens

Trigger: CAVEMAN_WRAP_PROVIDER=bedrock with AWS_SESSION_TOKEN set but neither AWS_ACCESS_KEY_ID nor AWS_SECRET_ACCESS_KEY; or only AWS_ACCESS_KEY_ID exported (or only the secret key) in the shell/CI environment.

Common situations: Partially populated CI/CD secrets; exporting keys across two shell sessions; assuming a session token alone is usable; a .env file where one of the pair was commented out.

Related errors


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

Appendix: source

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

  return kept.join("\n");
}

function bedrockCredentialEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined {
  const raw = env[name];
  if (typeof raw !== "string" || !raw.trim()) return undefined;
  if (/[\r\n]/.test(raw)) throw new Error(`${name} must not contain a newline`);
  return raw.trim();
}

function bedrockUpstreamCredentialFromEnv(env: NodeJS.ProcessEnv): string | undefined {
  const bearer = bedrockCredentialEnvValue(env, "AWS_BEARER_TOKEN_BEDROCK");
  if (bearer) return bearer;
  const accessKey = bedrockCredentialEnvValue(env, "AWS_ACCESS_KEY_ID");
  const secretKey = bedrockCredentialEnvValue(env, "AWS_SECRET_ACCESS_KEY");
  const sessionToken = bedrockCredentialEnvValue(env, "AWS_SESSION_TOKEN");
  if (!accessKey && !secretKey && !sessionToken) return undefined;
  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");
  }

View on GitHub (pinned to 3ee70a1026)