Skip to content
Ayhan Sipahi Ayhan Sipahi

Get the Agent Off the Laptop: Claude Code on AWS Lambda MicroVMs

Why coding agents belong in disposable cloud sandboxes, and what AWS Lambda MicroVMs change versus EC2 dev boxes, Fargate, Codespaces, and Claude Code's own sandbox.

A coding agent that runs shell commands inherits the shell it runs in: every key under ~/.ssh, every profile in ~/.aws/credentials, every token in the environment, and whatever egress the local network allows. The non-obvious part is that turning on an OS-level sandbox does not fix this. Claude Code’s sandboxing documentation is blunt about it: the default read policy “still allows reading credential files such as ~/.aws/credentials and ~/.ssh/” unless you add deny entries. Restricting what a command can write leaves untouched what it can read. My recommendation is a two-step default. Enable Claude Code’s built-in sandbox first, because it costs one settings file. Move to per-developer disposable cloud sandboxes only when you need credentials that never exist on the endpoint at all. AWS’s Lambda MicroVM reference implementation is the worked example for that second step: four patterns worth copying, a decision framework, the cost arithmetic, and the places where the sample stops short of production.

One disclaimer up front: the stack described here has not been deployed or measured. Every number is attributed to a published rate card, to a named third party, or to a file in the sample repository.

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, not only to what the model 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 part teams skip, 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.

The limits of a same-host boundary

You graduate when you need one of four properties the in-process sandbox cannot provide.

The first is credentials that never exist on the endpoint. A deny rule stops a sandboxed command from reading ~/.aws/credentials. It does not stop the file from sitting on a laptop that travels, and it does not shorten the key’s lifetime. The second is an egress chokepoint you own. Filtering by hostname inside the agent process is not the same as routing every packet through infrastructure where you can log flows and attach a firewall. The third is a uniform toolchain. With agents, version drift is worse than the familiar build problem, because the agent’s plan depends on which CLI versions it discovers. Node 20 on one machine and Node 22 on another produce different agent behaviour, not just different output. The fourth is one terminable object per session. When a developer leaves or a session goes wrong, “revoke the workspace” should be a single API call, not 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 will break more first deployments than anything else here. 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, not durable storage. 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 is the timeout on its checkpoint HTTP client, not an archiving budget. 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, not restored. 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, not floating tags:

{
  "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. Most internal developer platforms conflate the two and end up redeploying infrastructure to bump a CLI. 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. Image freshness is an operational metric, not a cosmetic one.

The decision framework

The recommended default is terminal-only, per-developer, Bedrock-backed disposable sandboxes on Lambda MicroVMs, reached only after the cheap answer has been ruled out.

No

Yes

No

Yes

No

Yes

Yes

No

No

Yes

Agent runs commands with credentials that matter?

Laptop plus built-in sandbox

Is no long-lived credential on the endpoint a hard requirement?

Claude Code sandbox with strictAllowlist

AWS account, MicroVM region, model access confirmed?

Codespaces or an 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 CodespacesClaude Code sandbox
Isolation boundaryFirecracker VM, service-managedFull VMContainer on a managed VMContainer on a hosted VMOS primitives, same host
Who patches the hostAWSYouAWSGitHubYou
Max continuous run8 h, then checkpoint and replaceUnboundedUnboundedIdle-timeout drivenUnbounded
Credential modelExecution role, temporaryInstance profileTask roleRepository secrets / OIDCYour laptop’s credentials
Egress control pointEgress connector into your VPCYour VPCYour VPCGitHub’s networkIn-process proxy allowlist
ArchitectureARM64 onlyAnyAnyAnyAny
Regions5 at GAAllAllProvider-managedn/a
Local Docker to buildNo, service-side buildAMI pipelineYesYesn/a
Third parties in data pathNone in terminal modeNoneNoneMicrosoft/GitHubNone

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.0000276944pervCPUsecondand0.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.0000276944 + 4 × 0.0000036667 per second, so roughly 0.252perhour,orabout0.252 per hour, or about 30 per developer per month at 120 active hours. Vertical scaling during bursts costs more. Snapshot storage adds 0.08perGBmonth,asuspendcosts0.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.18perhouranda4coreat0.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 the lowest-overhead option here, and the devcontainer spec is portable. 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 default was chosen to obtain.

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.

Common pitfalls

  • 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, not restriction. 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 no account grant, not broken IAM. 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.

The default and its boundary

Enable Claude Code’s built-in sandbox for every developer today, with strictAllowlist and explicit credentials deny entries, and treat that as the answer unless a specific requirement breaks it. The requirements that break it are narrow and real: no long-lived credentials on endpoints, an egress path you can inspect, a toolchain you version, and one revocable object per session. 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 default, with EC2 dev boxes as the override for GPU or long-running work and Codespaces as the override when hosted compute is already acceptable. None of it has been deployed or measured here, so treat the figures above as published rates rather than observed bills. 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