JuliusBrussee/caveman · error · Error

AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set toge

Error message

AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set together

What it means

In managed Bedrock wrap, bedrockUpstreamCredentialFromEnv builds the x-cave-upstream-key header from AWS credentials: AWS_BEARER_TOKEN_BEDROCK wins alone; otherwise SigV4-style keys are combined as 'accessKey:secretKey[:sessionToken]'. If any of the three key variables is set but the access key or secret key is missing (empty values count as unset), the pair is incomplete and credential construction is refused — a half-built credential would leak through as a broken header.

Source

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

  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 766dce6b13)

Solutions

  1. Export both halves of the pair: `export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...` (plus AWS_SESSION_TOKEN only alongside both), then rerun the wrap
  2. If you meant to use no static credentials, unset all three: `unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN`
  3. If you have a bearer token, use `export AWS_BEARER_TOKEN_BEDROCK=<token>` instead — it takes priority and needs no pair
  4. Audit where the lone variable came from (`env | grep AWS_`) so it does not reappear in later shells

Example fix

# before: only the secret key is set
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
caveman claude
# >> AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set together

# after: complete pair (or bearer token)
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
caveman claude
Defensive patterns

Strategy: validation

Validate before calling

const has = (n) => Boolean(process.env[n] && process.env[n].trim());
const bearer = has('AWS_BEARER_TOKEN_BEDROCK');
const pair = has('AWS_ACCESS_KEY_ID') && has('AWS_SECRET_ACCESS_KEY');
if (process.env.CAVEMAN_WRAP_PROVIDER === 'bedrock' && !bearer && !pair) {
  throw new Error('set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY together (or AWS_BEARER_TOKEN_BEDROCK), or unset all three');
}

Type guard

const isCompleteAwsKeyPair = (env) =>
  Boolean((env.AWS_ACCESS_KEY_ID?.trim()) && (env.AWS_SECRET_ACCESS_KEY?.trim()));

Try / catch

catch (err) { if (err.message.includes('must be set together')) { /* set the missing half of the pair or unset both; session token alone is never valid */ } else throw err; }

Prevention

When it happens

Trigger: `caveman wrap claude` with CAVEMAN_WRAP_PROVIDER=bedrock in managed mode, no AWS_BEARER_TOKEN_BEDROCK, and only one of AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY set (or only AWS_SESSION_TOKEN, e.g. from an assumed-role shell that exports just the session token).

Common situations: Reusing a shell where only the secret key leaked into env; CI jobs that inject session tokens without the key pair; aws sso logins exporting a subset of variables; copy-paste of one variable from a credentials file.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/e084268d21ff8b86. Report an issue: GitHub.