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
- 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
- If you meant to use no static credentials, unset all three: `unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN`
- If you have a bearer token, use `export AWS_BEARER_TOKEN_BEDROCK=<token>` instead — it takes priority and needs no pair
- 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
- Export AWS key material as a complete pair (plus optional session token), never piecemeal
- After `aws sso login` or role assumption, verify with `env | grep ^AWS_` that all three role vars landed
- Prefer AWS_BEARER_TOKEN_BEDROCK when your provider issues single-line bearer tokens
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
- ${name} must not contain a newline
- CAVEMAN_BEDROCK_ENDPOINT must be runtime or mantle
- managed Bedrock wrap requires a valid CAVE_API_KEY
- caveman build: set CAVE_MODEL when zero or multiple provider
- caveman-code: no supported provider credential found; set AN
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/e084268d21ff8b86.
Report an issue: GitHub.