Cross-posted with permission from iqraa.tech — the full guide has the complete IAM policy, SCP examples, and a 300-engineer rollout case study.
Claude Code has a Bedrock mode. Most people find out about it from a single environment variable — CLAUDE_CODE_USE_BEDROCK=1 — and assume that's the whole story. It isn't. The env var is the on-switch; the IAM policy, region strategy, and governance layer around it are where almost every enterprise rollout actually gets stuck.
I run through the deployment end to end below — the parts that matter for a platform team, not just "here's a flag."
Why route through Bedrock at all
The models are identical either way. What changes is procurement, governance, and audit:
- Consolidated billing. An AWS spend commitment absorbs Bedrock usage instead of opening a separate Anthropic contract.
- Data residency. Bedrock lets you pin inference to a region — direct Anthropic API gives you far less control over where a request lands.
- SCP-level guardrails. You can restrict which IAM principals, accounts, or regions may invoke Bedrock at all. That control doesn't exist on the direct API.
- CloudTrail auditability. Every invocation lands in CloudTrail with caller identity, source IP, region, and model ID — append-only and SIEM-ready.
The environment variables
export CLAUDE_CODE_USE_BEDROCK=1
export AWS_REGION=us-east-1
export ANTHROPIC_MODEL=us.anthropic.claude-sonnet-4-6-20250610-v1:0
export AWS_PROFILE=claude-code-prod
Note the model ID format difference — direct Anthropic uses claude-sonnet-4-6; Bedrock uses inference-profile IDs like us.anthropic.claude-sonnet-4-6-20250610-v1:0. The us. prefix means AWS load-balances the request across US regions for resilience. Drop the prefix and you're pinned to one region only. Mixing the two ID formats is the single most common "model not found" error teams hit on day one.
The IAM policy (minimum viable, not bedrock:*)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeClaudeModels",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": [
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-*",
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-*",
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-haiku-*"
]
},
{
"Sid": "UseCrossRegionInferenceProfiles",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": ["arn:aws:bedrock:us-east-1:*:inference-profile/us.anthropic.claude-*"]
},
{
"Sid": "ListFoundationModels",
"Effect": "Allow",
"Action": "bedrock:ListFoundationModels",
"Resource": "*"
}
]
}
Two details that trip people up:
-
InvokeModelWithResponseStreamis a separate permission fromInvokeModel. Miss it and Claude Code falls back to one-shot invocation — it still works, but feels sluggish and streaming sessions fail. - Never grant
Resource: "*"on Bedrock invoke actions. Bedrock also hosts Llama, Mistral, Nova, and marketplace models with their own cost/security profile. Scope toanthropic.claude-*ARNs only, so a leaked credential's blast radius stays limited to Anthropic models.
The gotcha nobody expects: model access provisioning
New AWS accounts don't have Claude models enabled by default. You have to explicitly request access per model per region in the Bedrock console, and approval can take anywhere from minutes to several hours — not instant, despite what the console implies. Request access to Haiku, Sonnet, and Opus up front, even if you're only using Sonnet today, so a future model switch doesn't get blocked by a multi-hour provisioning delay mid-rollout.
SCPs: the actual governance layer
IAM controls what a role can do; SCPs control what an entire AWS account can do, org-wide:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyBedrockOutsideApprovedRegions",
"Effect": "Deny",
"Action": "bedrock:*",
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:RequestedRegion": ["us-east-1", "eu-west-1"] }
}
},
{
"Sid": "DenyNonAnthropicBedrockModels",
"Effect": "Deny",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": "arn:aws:bedrock:*:*:foundation-model/*",
"Condition": {
"StringNotLike": { "bedrock:FoundationModelArn": "arn:aws:bedrock:*:*:foundation-model/anthropic.claude-*" }
}
}
]
}
Layer this with an IAM permission boundary and the inline invoke policy above, and you get the same three-layer model mature shops already use for IAM and KMS: SCP governs the org, permission boundary governs what roles developers can create, inline policy governs what Claude Code itself can invoke.
Cost levers that actually move the number
The headline per-token rate is the ceiling, not what you'll actually pay:
-
Prompt caching — cached tokens bill at ~10% of normal input rate. A stable
CLAUDE.md(no dynamic timestamps/build IDs) keeps cache hits high; that alone routinely cuts effective input cost 80-90% on long sessions. -
Model routing — Haiku input is $0.80/M tokens vs. Opus at $15/M. Review your actual model mix after 30 days; Opus dominating usually means either a
CLAUDE.mdthat isn't giving Sonnet enough structure, or developers manually overriding the default. -
--max-budget-usdin CI — without a hard cap, a runaway agent loop in a failed pipeline is the most common cost-overrun story in early adoption. $5/pipeline is a reasonable default for routine tasks.
Auth: use the credential chain you already have
Claude Code on Bedrock rides the standard AWS SDK credential chain — no custom auth to build. AWS SSO for developer workstations (aws sso login, short-lived creds, single point of revocation), OIDC federation for CI/CD (GitHub Actions/GitLab/Jenkins all support it — no static secrets), instance/task roles for EC2/ECS. If you're stuck on long-lived access keys, rotate quarterly via Secrets Manager + a rotator Lambda, not manually.
The five failure modes that generate the most support tickets
- Skipping the model-access request (blocks launch for hours)
- Wrong model ID format — direct vs. cross-region vs. single-region Bedrock IDs
- Missing
InvokeModelWithResponseStream - Wildcard IAM resources on Bedrock invoke actions
- No SCP region pinning — developers will invoke from unintended regions if nothing stops them
Full write-up (including a worked 300-engineer financial-services rollout, VPC endpoint config, and provisioned-throughput break-even math) is here: Claude Code AWS Bedrock: Enterprise Setup Guide
Happy to answer questions on IAM/SCP specifics in the comments — I set up a few of these rollouts and the model-access delay catches everyone the first time.
``
Top comments (2)
The inference-profile vs direct model ID mix-up being the number-one day-one error matches what I've seen — the us. prefix doing cross-region load-balancing is genuinely non-obvious. The SCP angle is the part most write-ups skip: as far as I know the direct API doesn't give you anything at the level of 'which principals may call this at all', which makes it a real differentiator beyond billing. Have you seen teams hit CloudTrail volume problems once per-invocation logging goes straight into a SIEM?
The inference-profile vs foundation-model ARN split is the single best catch here, that dual-statement policy is exactly what most published examples miss. One nuance on "the models are identical either way": the weights are, but harness features lag by surface. Prompt caching, cache TTL options and token-counting endpoints have all reached the direct API before Bedrock at various points, so a team benchmarking cost per task on direct Anthropic can see materially different cache-read economics after migrating. Worth a line in the governance doc so nobody treats the delta as a regression. Question about the 300-engineer rollout: how do you distribute and rotate the inference-profile IDs when a new model version lands? Do you use an SSM parameter that the shell profile reads, or a wrapper that resolves "latest approved" at session start? Hardcoded v1:0 strings in dotfiles seem like the thing that breaks quietly six months later.