Skip to content
Ayhan Sipahi Ayhan Sipahi

Where Should Claude Code Run? Devcontainer, Codespaces, or MicroVM

Devcontainers, Codespaces and AWS Lambda MicroVMs as homes for a coding agent: what each rung adds, what it costs, and when moving the agent off the laptop pays off.

A coding agent changes what a development environment is for. A long session installs packages, runs test suites, starts servers, and touches the network hundreds of times without a human approving each step. Three questions stop being theoretical at that point: whether every developer’s agent sees the same toolchain, whether you can throw the environment away the moment it goes wrong, and what the agent can actually reach from inside it. A devcontainer answers the first. Codespaces answers the first two. Neither moves your credentials into an account you control or gives you an egress path you can inspect, which is the gap per-developer cloud sandboxes fill. So the recommendation is a ladder: turn on Claude Code’s built-in sandbox today, add a devcontainer when reproducibility starts costing you, and move the whole environment into your own cloud only when those last two properties become requirements. AWS’s Lambda MicroVM reference implementation is the worked example for that last rung.

Claude Code’s built-in sandbox

The cheap answer ships today. Claude Code runs Bash commands inside an OS-enforced boundary: Seatbelt on macOS, bubblewrap on Linux and WSL2. Network egress is forced through a proxy that enforces a domain allowlist. Because the boundary is an OS primitive, it applies to every child process a command spawns, including the ones the model never typed.

A managed-settings configuration that closes the obvious holes looks like this:

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "network": {
      "strictAllowlist": true,
      "allowedDomains": ["*.github.com", "registry.npmjs.org"]
    },
    "credentials": {
      "files": [
        { "path": "~/.aws/credentials", "mode": "deny" },
        { "path": "~/.ssh", "mode": "deny" }
      ],
      "envVars": [
        { "name": "GITHUB_TOKEN", "mode": "deny" },
        { "name": "NPM_TOKEN", "mode": "deny" }
      ]
    }
  }
}

Four keys carry most of the weight. failIfUnavailable turns a missing bubblewrap into a startup failure instead of a silent fallback to unsandboxed execution. allowUnsandboxedCommands: false disables the escape hatch that lets the model retry a failed command outside the boundary. strictAllowlist denies non-allowlisted hosts instead of prompting, and requires Claude Code v2.1.219 or later. The credentials block is the easiest one to leave out, and it is the one that keeps the agent out of ~/.aws and ~/.ssh.

The trade-off is that this is a same-host boundary. The proxy makes its allow decision from the client-supplied hostname, and by default it does not terminate or inspect TLS. The documentation warns directly that broad entries such as github.com can create exfiltration paths through domain fronting. The experimental network.tlsTerminate setting (v2.1.199 and later) makes the proxy terminate TLS, but the same docs note it “does not add content filtering.” So the built-in sandbox reduces blast radius. It does not remove the credentials from the machine, and it does not give you a place to stand and watch traffic.

For a team whose agents touch a repository, a package registry, and nothing else, that is enough. The honest recommendation is to stop there.

What devcontainers and Codespaces solve

A devcontainer is the cheapest fix for the toolchain question, and toolchain drift matters more with agents than it did with people. The agent’s plan depends on which CLI versions it discovers, so Node 20 on one machine and Node 22 on another produce different agent behaviour. A devcontainer.json pins that surface and travels across editors. What it does not change is where the container runs. The Docker daemon is on your laptop, egress still leaves through your own network, and anything you mount in (a forwarded SSH agent socket, a bind-mounted ~/.aws) is as reachable from inside the container as outside it.

Codespaces takes the same definition and runs it on hosted compute. That buys real things: a workspace you can delete and recreate in minutes, no laptop fan, and prebuilds that make a cold start survivable. It also answers the credential question differently rather than fully. The secrets live in GitHub instead of on your disk, the compute is GitHub’s, and the agent’s traffic leaves through GitHub’s network.

So rank the rungs by what each one adds, and notice what none of them adds. Two properties stay out of reach until the environment runs in infrastructure you control. The first is credentials that live in your own account with minutes-long lifetimes, rather than long-lived keys parked somewhere convenient. The second is an egress chokepoint you own, where you can log flows and attach a firewall, instead of filtering by hostname inside the agent process. A third follows from both: one terminable object per session in your own account, so “revoke that workspace” is a single API call rather than an archaeology exercise across a disk, a keychain, and a shell history.

Compute isolation sits underneath the guardrail layer rather than replacing it. Prompt-injection defence and output filtering belong to AI agent security. What the agent reads at the repository level is a different axis again, covered in model-agnostic AI coding setup. The question here is narrower: where the process runs.

Lambda MicroVMs in brief

AWS Lambda MicroVMs reached general availability on 22 June 2026 in five regions: us-east-1, us-east-2, us-west-2, eu-west-1, and ap-northeast-1. Each MicroVM is a Firecracker-isolated virtual machine running Amazon Linux 2023, one per session. AWS positions it for code supplied by users or generated by AI.

The shape of the service matters more than the marketing. It is ARM64 only at launch. A single MicroVM tops out at 16 vCPUs, 32 GB of memory, and 32 GB of disk. Baseline sizes run from 0.5 GB to 8 GB, CPU is allocated at a 2:1 GB-to-vCPU ratio, and vertical scaling reaches 4x the baseline automatically. Deployment is image-then-launch: you upload a ZIP containing a Dockerfile to S3 and reference a Lambda-published base image. The service builds it, starts your app, waits on a /ready hook, then captures a Firecracker snapshot of disk and memory including running processes. There is no local Docker daemon in the deploy path. Lifecycle hooks are plain HTTP endpoints your app serves, registered in the sample under HOOK_PREFIX = "/aws/lambda-microvms/runtime/v1" for run, resume, suspend, and terminate.

Two constraints shape every design decision that follows. maximumDurationInSeconds accepts 1 to 28,800 seconds. The documented definition is the maximum duration the MicroVM can remain in a running or suspended state before Lambda terminates it, so suspended time counts. And TERMINATED is terminal: a terminated MicroVM cannot be resumed. Aidan Steele reported roughly 2 seconds from RunMicrovm to RUNNING and about 1 second each for suspend and resume. The primitive is fast; it is simply not permanent.

The AWS sample in aws-samples/anthropic-on-aws wires this into a working developer platform. The overall shape:

shell WebSocket

presigned URLs

Developer browser

Private connectivity

Private API Gateway

Control Lambda

DynamoDB sessions

MicroVM running Claude Code

VPC egress connector

VPC endpoints

NAT Gateway

Checkpoint bucket

By default MicroVMs get plain public egress through an AWS-managed connector. The sample replaces it with a customer-managed VPC egress connector. Private AWS traffic then reaches interface endpoints for logs, execute-api, and Bedrock, public HTTPS leaves through a NAT Gateway, and the connector security group allows TCP 443 only. The control API is a private API Gateway REST API. Its resource policy allows execute-api:Invoke when aws:SourceVpce matches the stack’s own endpoint, and explicitly denies it when it does not. The explicit deny is what makes the policy airtight rather than merely narrow.

Pattern 1: short-lived shell tokens instead of SSH

There is no SSH daemon, no bastion, no public IP, and no inbound application listener anywhere in the sample. Interactive access is a minted credential instead of an open port.

1. Browser calls the private control API over Cognito auth

2. Control Lambda calls CreateMicrovmShellAuthToken

3. Service returns an X-aws-proxy-auth token

4. Browser opens a WebSocket to SHELL_INGRESS directly

5. Token stays in JS memory and is re-minted on reconnect

The service exposes two distinct token APIs. CreateMicrovmAuthToken mints a token for the general HTTPS endpoint, scoped to a MicroVM, a set of allowed ports, and an expiry. CreateMicrovmShellAuthToken mints a token for interactive PTY access. It requires the MicroVM to have been launched with a SHELL_INGRESS connector attached, which the sample does in control-plane/src/service.ts by passing the managed ARN ending in network-connector:aws-network-connector:SHELL_INGRESS.

The detail worth copying is a hardening decision rather than a service feature. AWS documents a 60-minute maximum TTL for the general CreateMicrovmAuthToken API; the sample’s deployment guide describes the portal requesting a five-minute shell credential and re-minting on reconnect. That is a deliberate choice to spend a little availability for a much smaller window of exposure. The token never touches localStorage, the URL, or a downloaded file, and the Cognito ID token lives in tab-scoped sessionStorage.

The shape generalises beyond MicroVMs. Picture a control plane that mints a narrowly-scoped, minutes-long credential for exactly one resource, hands it to the client, and never persists it. That is strictly better than any long-lived bastion, whatever compute sits behind it.

Pattern 2: inference by execution role

Claude Code inside the MicroVM authenticates to Bedrock using the execution role’s temporary credentials, fetched from the container credentials endpoint at http://169.254.170.2. Claude Code uses the standard AWS SDK credential chain, so nothing needs patching. There is no interactive sign-in and no API key on any developer device. The agent only sets the provider flags:

def claude_provider_environment(session: Session) -> dict[str, str]:
    if session.inference_mode == "bedrock":
        model_id = session.bedrock_model_id or ""
        model = bedrock_model_selection(model_id)
        environment = {
            "CLAUDE_CODE_USE_BEDROCK": "1",
            "ANTHROPIC_MODEL": model,
        }
        if model != model_id:
            environment[
                f"ANTHROPIC_DEFAULT_{model.upper()}_MODEL"
            ] = model_id
        if model_id.startswith("anthropic."):
            environment["CLAUDE_CODE_USE_MANTLE"] = "1"
        return environment
    # ... remaining provider branches omitted

The model ID decides the endpoint. A direct ID such as anthropic.claude-sonnet-5 routes to the Mantle endpoint, which serves Claude models through the native Anthropic Messages API shape. A geographic or global inference profile ID with a us., eu., au., or global. prefix routes to Bedrock Runtime instead. The CDK stack validates the value against /^(?:(?:us|eu|au|global)\.)?anthropic\.claude-[A-Za-z0-9._:-]{1,180}$/, which accepts Claude IDs and nothing else.

The most copyable line in the whole stack is that the same statements appear twice: once on the IAM role and once on the VPC endpoint policy.

const invokeMantle = new iam.PolicyStatement({
  actions: ['bedrock-mantle:CreateInference'],
  resources: [mantleProjectArn],
});
microvmExecutionRole.addToPolicy(invokeMantle);
bedrockMantleEndpoint.addToPolicy(
  new iam.PolicyStatement({
    actions: ['bedrock-mantle:CreateInference'],
    principals: [microvmExecutionRole],
    resources: [mantleProjectArn],
  }),
);

Because the endpoint policy repeats the constraint, a compromised or over-broadened role still cannot reach a different model through that path. The same instinct, scoping access at the narrowest layer instead of handing over a broad tool surface, is the argument in skipping the MCP layer for scoped API access.

One gotcha is worth settling before you deploy anything. Mantle has its own model lineup and its own account-level access grants. Claude Code’s Bedrock documentation states that a 403 from Mantle with valid credentials means the AWS account has not been granted access to the requested model. Three official sources currently point in three directions: the sample defaults to anthropic.claude-sonnet-5, Claude Code’s own Bedrock docs give us.anthropic.claude-opus-5 as the primary default with the sonnet alias resolving to Sonnet 4.5, and Serverless Land’s equivalent pattern preconfigures Sonnet 4.6. Confirm the model in your account before deploying, and pin it explicitly rather than inheriting a default.

Pattern 3: checkpoint and restore around a hard ceiling

Two different things in this system are called a checkpoint, and confusing them is how you lose work.

The first is the Firecracker snapshot the service takes. It serialises guest memory, vCPU, and device state, then restores through a copy-on-write mapping of the memory file. Suspend and resume preserve running processes and open buffers. AWS describes the restored workspace as returning exactly as it was left, with no re-computation.

The second is a tar archive of /workspace that the sample’s own agent uploads to S3. It exists because of the ceiling above: eight hours counting suspended time, and no resume once a MicroVM is TERMINATED. Both AWS statements are true, and they read as contradictory until you separate them. The platform gives you stateful compute for up to eight hours; durability has to come from somewhere else. Any workspace expected to outlive a working day needs file-level checkpointing layered on top.

The sample builds exactly that. A reconciler on a one-minute EventBridge schedule begins a managed termination DEFAULT_EXPIRATION_LEAD_SECONDS = 45 * 60 before expiry, leaving room for the /terminate hook to finish uploading. The in-VM agent archives /workspace before suspend, restart, and terminate; CHECKPOINT_TIMEOUT_SECONDS = 50 looks like an archiving budget but is the timeout on its checkpoint HTTP client. The archive lands in a versioned, KMS-encrypted bucket with a 90-day non-current version expiry.

The access path is the interesting part. The MicroVM execution role has no direct S3 permission on that bucket at all. The agent calls POST /sessions/{id}/checkpoint-urls on the private control API and receives presigned URLs, refreshed every REFRESH_URLS_AFTER_SECONDS = 15 * 60. Its execute-api:Invoke grant is scoped to that single route:

microvmExecutionRole.addToPolicy(
  new iam.PolicyStatement({
    actions: ['execute-api:Invoke'],
    resources: [
      api.arnForExecuteApi(
        'POST',
        '/sessions/*/checkpoint-urls',
        api.deploymentStage.stageName,
      ),
    ],
  }),
);

Restore rehydrates into a fresh MicroVM. Extraction is bounded by MAX_ARCHIVE_MEMBERS = 200_000 plus byte caps, a zip-bomb guard worth copying verbatim. What does not come back: running processes, memory, open terminals, temporary credentials, the VS Code Server binaries, the tunnel identity, and /home/developer. Those are recreated from scratch. Git remains the source of record.

The eight-hour ceiling reads as a limitation and behaves as a forcing function. A workspace that cannot survive a day turns uncommitted work into a known, dated risk rather than an ambient one. That is a healthier default than a dev box left running for months with a dirty tree.

Pattern 4: toolchain rebuilds without a platform redeploy

Tool versions are pinned with SHA-256 checksums:

{
  "claudeCode": {
    "version": "2.1.215",
    "sha256": "2b43a3d5b0787217e5d7381fad42c7314292546fe9db9eb8b9b379de90509b30"
  },
  "vscodeCli": {
    "version": "1.129.1",
    "commit": "8a7abeba6e03ea3af87bfbce9a1b7e48fed567b8",
    "sha256": "abd6e9ef317be8ecbbe255954bb76e5c174f15e1b37cf99d82a3d59b798812a6"
  }
}

A provisioning script uploads a new source archive, waits for the new image version to reach ACTIVE, and updates two SSM parameters holding the image ARN and the network connector ARN. Existing running or suspended environments stay on the version they started with. New environments pick up the active version, and a Restart checkpoints an existing workspace and replaces it from the active image.

That separation is the point: “the platform changed” is a CDK deploy, and “the toolchain changed” is an image provision. Conflating the two is what turns a CLI version bump into an infrastructure redeploy. The same instinct drives ephemeral CI runners; the Claude Code PR reviewer setup is the CI-side version.

Two constraints make this less flexible than it looks. Environment variables are baked into the image, unlike Lambda functions, so changing one means rebuilding. Anything that varies per MicroVM has to travel through the run hook payload, capped at 16 KB. Steele reports image builds taking two to three minutes, with roughly 7.2 GB of free disk during the build. That is an undocumented ceiling on how much toolchain you can bake in. Worth noting: the sample pins Claude Code 2.1.215, while strictAllowlist requires 2.1.219, so image freshness is an operational metric.

The decision framework

Each rung costs more than the one below it, so climb only as far as your requirements push you. The top rung here is terminal-only, per-developer, Bedrock-backed sandboxes on Lambda MicroVMs.

No

Yes

No

Yes

No

Yes

Yes

No

No

Yes

Does every agent need the same pinned toolchain?

Laptop plus Claude Code sandbox

Devcontainer plus Claude Code sandbox

Must credentials and egress stay in an account you control?

GitHub Codespaces

AWS account, MicroVM region, model access confirmed?

EC2 dev box in your region

Need GPU, heavy nested Docker, or over 8h continuous?

EC2 dev box via SSM Session Manager

Lambda MicroVM sandboxes, terminal only

Third-party relay approved for source traffic?

Stay on terminal mode

Add VS Code Remote Tunnels mode

DimensionLambda MicroVMEC2 dev boxECS/FargateGitHub CodespacesLocal devcontainerClaude Code sandbox
Isolation boundaryFirecracker VM, service-managedFull VMContainer on a managed VMContainer on a hosted VMContainer on your laptopOS primitives, same host
Who patches the hostAWSYouAWSGitHubYouYou
Max continuous run8 h, then checkpoint and replaceUnboundedUnboundedIdle-timeout drivenUnboundedUnbounded
Credential modelExecution role, temporaryInstance profileTask roleRepository secrets / OIDCMounted from your laptopYour laptop’s credentials
Egress control pointEgress connector into your VPCYour VPCYour VPCGitHub’s networkYour local networkIn-process proxy allowlist
ArchitectureARM64 onlyAnyAnyAnyAnyAny
Regions5 at GAAllAllProvider-managedn/an/a
Local Docker to buildNo, service-side buildAMI pipelineYesYesYesn/a
Third parties in data pathNone in terminal modeNoneNoneMicrosoft/GitHubNoneNone

State the limit plainly, because AWS does: the sample’s README says the sandbox “still needs explicit IAM, network, and data controls” and that the sample “does not treat the MicroVM boundary as a substitute for least privilege or egress policy.” The isolation boundary is not the security control. The role with one log group, one model ARN, and one API route is the security control.

The cost arithmetic

Lambda MicroVM compute on ARM in us-east-1 is billed per second at $0.0000276944 per vCPU-second and $0.0000036667 per GB-second. The sample sizes each workspace at 4 GB, from an abridged deployment.example.json:

{
  "region": "us-east-1",
  "vpcCidr": "10.42.0.0/16",
  "projectName": "claude-microvm",
  "trustedClientCidr": "10.100.0.0/22",
  "inferenceMode": "bedrock",
  "bedrockModelId": "anthropic.claude-sonnet-5",
  "idleAfterSeconds": 900,
  "suspendedRetentionSeconds": 3600,
  "microvmMemoryMib": 4096
}

At the 2:1 ratio, 4 GB gives 2 vCPUs. That is 2 × $0.0000276944 + 4 × $0.0000036667 per second, so roughly $0.252 per hour, or about $30 per developer per month at 120 active hours. Vertical scaling during bursts costs more. Snapshot storage adds $0.08 per GB-month, a suspend costs $0.0038 per GB written, and a resume or launch costs $0.00155 per GB read. Billing is per second rather than per millisecond, which as Yan Cui notes puts the pricing model closer to Fargate than to Lambda.

For comparison, GitHub Codespaces bills a 2-core machine at $0.18 per hour and a 4-core at $0.36, plus $0.07 per GB-month of storage. A 2 vCPU MicroVM sits between them on compute alone.

Compute is not where the surprise lives. The shared platform adds a NAT Gateway plus data processing, and three to five interface VPC endpoints billed hourly per availability zone. Those are fixed costs regardless of how many developers use the platform. Verify both against the current VPC pricing page before you build a business case.

The largest hidden cost is not on any rate card. The control API is private, so reaching it requires organization-managed private connectivity: Client VPN, Direct Connect, Transit Gateway, or a routed VDI, plus private DNS to the execute-api endpoint. The stack creates none of it. For a small team without existing private routing, that prerequisite dominates every other line item.

Override cases

Reach for an EC2 dev box when the agent needs a GPU, an x86 build, sustained nested Docker, or more than eight hours of continuous compute. SSM Session Manager already gives SSH-less, CloudTrail-logged access, so the access-path argument mostly evaporates. What you take back is the AMI pipeline, the patching, and the drift you were trying to escape, plus paying for idle unless you build stop/start automation.

Reach for ECS or Fargate when you already operate a container platform and want scheduling control. You keep your VPC and your egress posture, and you give up the per-session VM boundary and the service-managed snapshot semantics.

Reach for Codespaces when the repository is on GitHub and your data-residency posture already accepts hosted compute. It is by far the lowest-overhead option here: no VPC, no private connectivity, no image pipeline, and the devcontainer definition you write is portable if you later move. The failure mode is precise: compute and source sit outside your AWS account and outside your egress controls, which is exactly the property the MicroVM rung was chosen to obtain. Stay on a local devcontainer when reproducibility was the only problem you had, because everything above it is infrastructure you now operate.

Reach for self-managed Firecracker on EC2 only when you need custom kernel or device behaviour and have someone to own it.

VS Code Desktop mode deserves its own line, because it is an architectural fork rather than a UI preference. Terminal mode never starts VS Code Server or a tunnel at all. Desktop mode routes source, terminal, and editor protocol traffic through a Microsoft-operated dev tunnels relay. It also adds a second identity per developer, deliberately unlinked from Cognito. Microsoft documents the relay as authenticated and encrypted, and VS Code’s tunnels documentation adds that an SSH connection is created over the tunnel for end-to-end encryption. Security researchers, SentinelOne’s Operation Digital Eye analysis among them, document the same capability as a persistence and command-and-control technique, precisely because the traffic looks like legitimate Microsoft infrastructure. Both descriptions are accurate. The enterprise decision is a single allow-or-deny call on global.rel.tunnels.api.visualstudio.com, and the sample is right to treat relay approval as a deployment prerequisite. If MCP tooling is in scope, an AgentCore Gateway endpoint is an optional path; AgentCore in production covers that layer.

Traps already documented

  • Treating the VM boundary as the security control. The MicroVM contains untrusted code; it does not decide what that code is allowed to call. Keep least privilege on the execution role and put an egress policy in front of NAT.
  • Calling an open NAT Gateway “egress control”. The sample’s connector security group allows 443 to 0.0.0.0/0. Routing egress through your VPC buys a chokepoint and flow logs, nothing more. AWS Network Firewall with domain-list rules, or centralized egress through an inspection VPC, is the part you still have to build.
  • Assuming DNS works inside nested containers. All outbound UDP is blocked by default, so containers fall back to public resolvers and fail quietly. Steele’s fix is to point them at Lambda’s resolver: docker run --dns 169.254.169.253.
  • Forgetting that suspended time counts. A workspace suspended overnight still burns against maximumDurationInSeconds. For long gaps, terminate and rely on checkpoint and restore rather than suspending.
  • Reading “checkpoint” as “save”. In-flight state is lost. Tell developers Git is the source of record, and consider an agent hook that commits to a scratch branch before the reconciler’s 45-minute window opens.
  • Trusting the sample’s default model ID. A 403 from Mantle with valid credentials means a missing account grant, so skip the IAM debugging. Confirm the model, then pin it.
  • Expecting environment variable changes to be cheap. Changing one means rebuilding the image and restarting workspaces. Use the run hook payload for anything per-MicroVM.
  • Assuming your ENI inventory is complete. Steele found that DescribeNetworkInterfaces omits connector ENIs unless you pass IncludeManagedResources=true, so network audits under-report until you fix them.
  • Shipping one NAT Gateway. The sample deploys natGateways: 1 to keep the example cheap. Production wants one per availability zone or centralized egress.
  • Planning for x86 or a sixth region. ARM64 only, five regions at GA. Check both before committing a roadmap.

Picking a rung

Enable Claude Code’s built-in sandbox for every developer today, with strictAllowlist and explicit credentials deny entries. Add a devcontainer when agents start behaving differently on different machines. Move to Codespaces when the laptop itself is the constraint and hosted compute is acceptable to your organization. The requirements that push you past Codespaces are narrow and real: credentials that live in your own account, an egress path you can inspect, and one revocable object per session that you own. When those apply and you are on AWS in a launch region with confirmed model access, per-developer Lambda MicroVM sandboxes in terminal-only mode are the right answer, with EC2 dev boxes as the override for GPU or long-running work. The next concrete step is also the cheapest verification: confirm the exact model ID you would pin is granted in the target account. aws bedrock list-inference-profiles answers that only for inference-profile IDs; a Mantle-format ID like the sample’s default never appears there, so the check is a question for your AWS account team, and a 403 with valid credentials means no grant. That single check decides whether the rest of the platform is worth planning.

References

Related posts