Host a Claude Agent SDK Loop on AWS in Fifteen Lines. Then Read the Contract Twice.

Hosting a Claude Agent SDK loop on Bedrock AgentCore Runtime really is one decorator; the value is the contract you sign (surface, dispatch, isolation) and the two walls that kill a first deploy (ARM64 and a sync handler that starves /ping).

Rick Hightower

Cover image for “Host a Claude Agent SDK Loop on AWS in Fifteen Lines. Then Read the Contract Twice.” by Rick Hightower

Bedrock AgentCore Runtime does not care what framework you use. It cares that your entrypoint speaks JSON on /invocations, stays healthy on /ping, and builds for ARM64. Miss any of those and a working laptop agent dies on deploy.

The pleasure of turning a laptop script into session-isolated infrastructure in about fifteen lines is real. It dies the moment your first deploy fails for a reason that has nothing to do with agents.

In this article: You will take a Claude Agent SDK query() loop that runs on your laptop and host it on Amazon Bedrock AgentCore Runtime with a single decorator. You will learn the Runtime contract (what it promises, what it demands), how to keep model calls inside Bedrock, how the AgentCore CLI deploys the fast path, and the two gotchas that kill first deploys: ARM64 images and synchronous handlers that starve the health check.

There is a specific kind of relief when an agent that only existed as a script becomes infrastructure. Endpoint. Session isolation. Scaling story. Same loop, different home. With Bedrock AgentCore Runtime, that lift is roughly fifteen lines of Python around a decorator. That is not marketing copy. It is the actual diff.

It is also why the interesting content is not the fifteen lines. Those lines sign a contract. AgentCore Runtime makes a specific promise and demands a specific shape in return. Every production control you hang on this host later (streaming, session lifecycle, multi-agent fan-out, hardening) rides on that shape. Read the decorator, then read the contract.

Then deal with ARM64. That is where your first deploy dies.

The loop on your laptop

Start with the agent, before any AWS is involved. The Claude Agent SDK runs the Claude agent loop inside your own Python process. One async generator, query(), handles reasoning, tool calls, permissions, hooks, sessions, and subagents. You iterate it, and messages come out.

Here is the embryo of a market-intelligence agent: read a saved competitor page, summarize what changed.

The SDK owns the loop. You declare tools once, consume messages, and print the final result when it arrives.

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    options = ClaudeAgentOptions(allowed_tools=["Read", "Bash", "Glob"])  # ①
    async for message in query(  # ②
        prompt="Read ./pricing-snapshot.html and summarize the pricing tiers.",
        options=options,
    ):
        if hasattr(message, "result"):
            print(message.result)  # ③

asyncio.run(main())

① Tool access is declared once on ClaudeAgentOptions; the SDK only exposes the tools you list. ② query() is an async generator: the SDK owns the agent loop, and you only consume messages from it. ③ A final result arrives on a message that carries .result; everything before that is intermediate traffic.

Note: The full extracted listing at code/agent-core/part-02-first-hosted-loop-claude-agent-sdk/listings/01-laptop-query-loop.py is the complete runnable program.

That is a complete agent. It has a loop, tools, and a stopping condition. It runs on your machine with your filesystem, your network, and your blast radius. Every failure mode you already know how to create is available to it.

What it does not have is anywhere to live. No endpoint, no session isolation, no scaling, no trace, and no story for forty analysts hitting it at 9 a.m. on Monday. That gap is the entire pitch of the next fifteen lines.

The lift: one decorator, same loop

BedrockAgentCoreApp is a Starlette server that calls your function. That is the whole abstraction, and its plainness is the point. The Runtime does not know or care that your function happens to be a Claude Agent SDK loop.

from bedrock_agentcore import BedrockAgentCoreApp
from claude_agent_sdk import query, ClaudeAgentOptions

app = BedrockAgentCoreApp()  # ①

@app.entrypoint  # ②
async def market_intel_agent(request):
    """request is the JSON body POSTed to /invocations."""
    prompt = request.get("prompt") if isinstance(request, dict) else str(request)  # ③
    options = ClaudeAgentOptions(allowed_tools=["Read", "Bash", "Glob"])
    final = None
    async for message in query(prompt=prompt, options=options):  # ④
        if hasattr(message, "result"):
            final = message.result
    return {"result": final}  # ⑤

if __name__ == "__main__":
    app.run()  # ⑥

BedrockAgentCoreApp is the Starlette host; it owns POST /invocations and GET /ping. ② @app.entrypoint registers your function as the invocation handler. The Runtime only sees this surface. ③ The request body is unwrapped into a prompt string so the same query() call as the laptop version can run. ④ The Claude Agent SDK loop is unchanged: same query(), same options, same tools. ⑤ Returning a dict is the JSON-out half of the contract; async functions collect a final result rather than streaming. ⑥ app.run() starts the local server for agentcore dev and for container entry.

Note: The full extracted listing at code/agent-core/part-02-first-hosted-loop-claude-agent-sdk/listings/02-agentcore-entrypoint.py is the complete runnable program.

Look at what changed and what did not. The loop is untouched: same query(), same options, same tools. What was added is a decorator, a request unwrap, and a return value. Your harness stayed yours. The hosting became AWS's.

From a laptop Claude Agent SDK loop through a thin decorator lift into Bedrock AgentCore Runtime: invocations, health, and a session microVM.

This is the foundational recipe the AgentCore README and Developer Guide show for every framework. Swap the body for a different agent later and the shape stays identical. That is the framework-agnostic claim being literally true rather than aspirationally true.

The contract underneath

Now the part worth reading twice, because everything downstream depends on it.

BedrockAgentCoreApp extends Starlette and registers exactly two things: POST /invocations and GET /ping. The contract is JSON in, and JSON or SSE out, on /invocations. Health is /ping, which returns Healthy or HealthyBusy. That is the surface. There is no magic, and there is nothing else to learn.

The interesting behavior is dispatch. The app's _invoke_handler inspects your entrypoint and treats three shapes differently:

  • An async function runs on a worker event loop, which keeps /ping responsive while your agent thinks.
  • An async generator gets bridged through a dedicated worker loop and returned to the client as an SSE stream.
  • A sync function is dispatched too, and this is where people get hurt.

Notice the design intent: the Runtime works hard to keep the health endpoint answering while your loop does something slow, because a loop that runs for minutes is the normal case, not the exception. AgentCore is built for long-running agents, and the dispatch logic is where you can see that belief in the code.

The Runtime contract: clients hit POST /invocations and GET /ping; dispatch routes async functions, async generators, and sync functions differently.

The entrypoint above is an async function, so it collects the final result message and returns JSON. Because query() is itself an async generator, you can instead return the generator and stream tokens out over SSE, which is what you actually want for a loop that runs for minutes. Get the blocking JSON version deployed first. Streaming, request context, and session threading are a deeper layer once the host is real.

Pattern check: Runtime is the isolation pattern, sold by the hour. Each session gets a dedicated microVM with isolated CPU, memory, and filesystem. Notice what it does not give you: the microVM bounds your blast radius, but nothing here bounds your loop. Turn limits, budget caps, and stopping conditions are still code you write. The platform's 8-hour ceiling is a backstop, not a control.

Keep the model call inside AWS

One environment variable decides whether your agent phones Anthropic directly or routes inference through Bedrock:

export CLAUDE_CODE_USE_BEDROCK=1

With that set and AWS credentials available, the Claude Agent SDK serves its model calls from Amazon Bedrock. Without it, you need ANTHROPIC_API_KEY in the container and your traffic leaves AWS.

For most teams, Bedrock is the answer, and not because the tokens are cheaper. It is single-vendor credentialing, since IAM already governs the rest of your stack; billing that lands in the account your finance team already reads; and access to Bedrock-side features like guardrails and cross-region inference sitting underneath a loop that does not know they are there. It also means an agent running in an AgentCore microVM talks to a model in the same cloud, which is a shorter answer to give your security review than the alternative.

In production: this variable is the difference between "the agent is an AWS workload" and "the agent is an AWS workload with an egress dependency and a second secret to rotate." Decide deliberately, once, at the start.

Deploy it with the AgentCore CLI

Use the AgentCore CLI, and be careful here, because most tutorials you will find still teach a different tool that is now dead.

npm install -g @aws/agentcore
agentcore create        # interactive wizard: framework, language, scaffolding
cd my-agent
agentcore dev           # local uvicorn with hot reload
agentcore deploy
agentcore invoke '{"prompt": "Read ./pricing-snapshot.html and summarize the tiers."}'

Yes, npm, for a Python agent. The CLI is a Node package that scaffolds and deploys projects in Python or TypeScript. Accept it and move on.

The full command set is create, deploy, dev, invoke, add, remove, logs, traces, and evals. Two of those are worth noticing now. agentcore dev gives you a local server with hot reload, which is the difference between iterating in seconds and iterating in deploys. And agentcore evals exists, which tells you something about where this platform thinks the hard part is.

The tool you should not use: the older Python Starter Toolkit (pip install bedrock-agentcore-starter-toolkit) exposed agentcore configure -e agent.py and agentcore launch. Its CLI is no longer supported. AWS's own repo now says so at the top of the README. If you have it installed, uninstall it before installing the new one, because they both claim the agentcore command name and the resulting conflict is a bad afternoon. If you have an existing project, agentcore import --source path/to/.bedrock_agentcore.yaml migrates it into a CLI-managed CloudFormation stack without re-creating resources.

Prerequisites are short: Python 3.10 or newer, Node for the CLI, AWS credentials configured, and a Claude model enabled in the Amazon Bedrock console.

This is still the fast path, and it is honest about being one. Production deployment is bring-your-own-container through ECR: build the ARM64 image, push it, and reference containerConfiguration.containerUri from CDK or Terraform rather than from a CLI on someone's laptop. For now, get something running.

Container facts, stated once

These come from the Developer Guide. They apply to every agent you host this way:

  • ARM64 is required for all deployed agents.
  • The container listens on 0.0.0.0:8080 and exposes POST /invocations and GET /ping.
  • Each session runs in a dedicated microVM with isolated CPU, memory, and filesystem.
  • Session states are Active, Idle, and Terminated.
  • Sessions terminate on 15 minutes of inactivity, at 8 hours maximum lifetime, or when unhealthy.
  • Payloads cap at 100 MB.

Two of those bite early. The 15-minute inactivity termination surprises people building agents that wait on humans, because a session sitting idle waiting for an approval is a session on a countdown. And the 8-hour ceiling is the outermost bound on any loop you host here, which means a genuinely long-running agent is a checkpoint-and-resume design problem, not a timeout you can raise.

AgentCore session lifecycle: Active and Idle transitions, terminated by inactivity, the 8-hour ceiling, or an unhealthy health check.

Gotcha: ARM64 is where your first deploy dies

Here is the wall. ARM64 is required, your laptop is probably x86, and the failure does not announce itself as an architecture problem. It announces itself as your agent working perfectly in local testing and then failing on deploy for reasons that appear to have nothing to do with agents.

The build itself is a flag:

docker buildx build --platform linux/arm64 -t my-agent .

The part that actually bites is subtler, and it is specific to hosting the Claude Agent SDK rather than a pure-Python framework. The SDK drives the Claude Code engine, which means it has native-runtime requirements beyond the Python package. Whatever your installed version needs has to be present and working on linux/arm64 in your image. This is documented as the most common friction point for this exact combination, and it is the reason a first deploy fails while a local run succeeds.

ARM64 deploy path: a successful laptop test still fails if you forget linux/arm64; build for ARM, emulate, verify native SDK deps, then deploy.

The fix is not clever; it is procedural: build ARM64 from the start, run the image locally under emulation before you push, and verify the SDK's native requirements against the version you actually pinned rather than the version the docs assumed. If you skip this and deploy on Friday, you will learn about buildx on Saturday.

Gotcha: the sync handler that starves the health check

The second wall is quieter, and it is a real production incident rather than a build error.

The Runtime dispatches async functions on a worker event loop specifically so /ping keeps answering while your agent works. Write a synchronous entrypoint that blocks, and you are fighting that design. Sessions terminate when unhealthy, so a health check that stops answering is not a monitoring inconvenience; it is your agent getting killed mid-task by the platform that is hosting it.

Async entrypoints keep /ping answering while the agent loop runs; a blocking sync handler starves the control plane and the Runtime terminates the session as unhealthy.

Write async def. Iterate query() with async for. If you have a genuinely blocking call inside your loop, push it off the event loop rather than letting it sit on top of the one thing keeping your session alive.

This is worth naming precisely, because it is the first instance of a pattern that recurs in production harness work: the platform's control plane and your agent's data plane share a process, and when your agent's work starves the platform's policy machinery, the platform makes a policy decision you will not like. Keep the two planes from fighting over the same event loop.

Do this today

  • Take a working Claude Agent SDK query() script and wrap it with BedrockAgentCoreApp and @app.entrypoint as an async def that returns {"result": ...}.
  • Set CLAUDE_CODE_USE_BEDROCK=1 (or deliberately choose Anthropic egress) and confirm which path your credentials support.
  • Install @aws/agentcore, not the deprecated Python starter toolkit; run agentcore dev before you touch deploy.
  • Build with docker buildx build --platform linux/arm64 and run the image under emulation once before the first push.
  • Invoke with a real prompt against /invocations and confirm /ping stays healthy for the full duration of the run.

Fifteen lines signed a contract

You now have an agent loop you wrote, running in a microVM you did not build, behind an endpoint you did not write, serving a model from the same cloud, with isolation you did not configure. The harness components that are infrastructure came from the platform. The loop stayed in your repo, under your tests, where you can still put a stopping condition in it.

That is the "rent the substrate" path in its smallest working form.

One framework example does not prove framework-agnostic hosting by itself. The durable lesson is the contract: JSON and SSE on /invocations, health on /ping, session-isolated microVMs, ARM64 images, and async handlers that refuse to starve the control plane. Sign that contract cleanly, and the same host can carry a different loop tomorrow without rewriting the infrastructure around it.

The lift was easy. The contract is what keeps the next deploy from teaching you architecture on a Saturday.