Deploying an agent means moving the same agent loop you ran on your laptop into a place that runs without you watching it. Three things change: where the code runs, how it gets its secrets and tools, and how you bound what it is allowed to do when nobody is there to click "approve".
On your machine the agent borrows a lot for free. It uses your shell, your files, your API key sitting in your shell profile, and you, the human, approving each risky step. A server has none of that. The job of a first deployment is to hand the agent each of those things on purpose, and nothing more.
What changes when an agent leaves your laptop?
Your laptop run and your deployed run are the same agent loop. The loop calls the model, the model asks to run a tool, the tool runs, the result goes back, repeat until done. The Claude Agent SDK runs that loop for you in either place. What differs is everything around the loop.
So a deployment is four decisions: pick a host, supply the secret, fix the tool list, and capture the output. Take them one at a time.
Where does a deployed agent run?
You have three broad options. Pick the smallest one that fits.
| Host | What it is | Good first fit |
|---|---|---|
| A container | Your code in an image you run anywhere | Yes, most teams start here |
| A CI job | The agent runs inside a pipeline step | Good for code tasks on a repo |
| Managed agents | Anthropic runs the loop and the sandbox | When you do not want to run infra |
A container is the common starting point because it is portable and you control it. You build an image, set environment variables, and run it. The agent works on files and services inside that container, not on your laptop.
The other end of the spectrum is letting Anthropic host the loop for you. With managed agents the loop and a per-session sandbox run on Anthropic infrastructure, and your app sends events and reads back results over a REST API. A common path is to prototype with the SDK locally, then move to managed agents once you do not want to operate sandboxes yourself.
For a first deployment, start with a container running the SDK. Everything below assumes that.
How does a deployed agent get its API key?
On your laptop the agent reads ANTHROPIC_API_KEY from your shell. The SDK does
the same thing on a server: it reads the key from an environment variable.
# the SDK reads this from the environment, same as on your laptop
export ANTHROPIC_API_KEY=your-api-keyThe change is where that value comes from. Hardcoding the
key is the most common deployment mistake, and it is a real one. Never put the
key in your Dockerfile, your pipeline YAML, or any file you commit. Anyone who
can read the file, or the image layers, or the git history, then has your key.
Instead, inject it at run time from whatever your host already provides:
- In CI, use the pipeline's secret store (for example a repository or organisation secret) and expose it to the step as an environment variable.
- In a container platform, set it as a secret environment variable on the service, not baked into the image.
- In a cloud, read it from the platform's secrets manager at start-up.
If you run Claude on a cloud provider instead of the Anthropic API directly, you
set a different flag and use that cloud's normal credentials rather than an
Anthropic key. For example, Amazon Bedrock uses CLAUDE_CODE_USE_BEDROCK=1 plus
your AWS credentials, and Google Vertex AI uses CLAUDE_CODE_USE_VERTEX=1 plus
your Google Cloud credentials. The principle is identical: the secret lives in the
platform, the agent reads it at run time, nothing is committed.
A practical habit: rotate the key on a schedule, and rotate it immediately if you think it leaked. A deployed agent runs unattended, so a leaked key can be abused for a long time before anyone notices.
How does a deployed agent get its tools?
A tool is anything the agent can do beyond talking: read a file, run a shell command, search the web, call your service. On your laptop you gave it broad access because you were watching. Unattended, you give it the smallest set the job needs.
You declare the allowed built-in tools as a plain list. The names are the same as
in Claude Code: Read, Write, Edit, Bash, Glob, Grep, WebSearch,
WebFetch. A read-only reviewing agent, for example, gets only the tools that
read:
{
"allowedTools": ["Read", "Glob", "Grep"]
}That single list is your strongest safety control. An agent that has never been
given Bash or Write cannot run a command or change a file, no matter what the
model decides it wants to do. Start from nothing and add only what the task
proves it needs.
When the agent needs to reach a system you own, a database or your own API, you do not hand it raw credentials. You expose that system as an MCP server and let the agent call it through that. MCP is a uniform interface for tools and data, so the same wiring works locally and in production. You list the servers the agent may connect to as config:
{
"mcpServers": {
"store-db": {
"command": "npx",
"args": ["-y", "@your-org/store-db-mcp"]
}
}
}In a deployment, treat an MCP server like any other dependency: it needs its own secrets (a database URL, an API token), and those come from the environment too, never from a committed file. If you connect untrusted or third-party MCP servers, read MCP security basics first, because a tool the agent can call is a tool an attacker can try to reach through the agent.
How do I limit what an agent can do when nobody is watching?
On your laptop, a permission prompt is your safety net: the agent pauses and asks before doing something risky. A server has no human to answer that prompt. So an unattended agent must never depend on one.
The SDK ships permission modes that set how cautious the loop is:
- default · ask before anything risky
- acceptEdits · auto-approve file edits, still gate the rest
- plan · explore and propose, change nothing
- bypassPermissions · skip prompts entirely
For a deployment, the combination that matters is a tight allowedTools
allow-list plus a mode that does not hang waiting for input. The default mode
is fine when you pair it with a permission callback, a function the SDK calls for
anything not already resolved by your allow-list, so it returns a yes or no in
code instead of waiting for a person.
Never use bypassPermissions outside an isolated sandbox. It removes
every guardrail at once, which is exactly what you do not want on a host that
touches real systems. If you think you need it, the safer answer is usually a
shorter allowedTools list and a sandbox with no network and no access to
anything outside the task.
The principle is fail closed. Anything you did not explicitly allow should be denied automatically, not left to a prompt that nobody will answer.
The smallest safe first deployment
Here is a first deployment that is useful and hard to get wrong: a read-only agent that reviews a codebase and writes a report. It cannot edit files, cannot run commands, and cannot reach the network beyond the model. The worst case is a wrong opinion in a text file.
You can prove the exact shape of the run from the CLI before you wrap it in a
service. Claude Code runs the same loop in one shot with -p (print mode), and a
trailing tool allow-list:
# one-shot, read-only, prints the result and exits
claude -p "Review the auth module for security issues. Write findings to review.md" \
--allowedTools "Read,Glob,Grep,Write"Print mode is the bridge between your laptop and a server: it takes a prompt, runs to completion, and exits with output you can capture. That is exactly what a host needs.
Use a tight, explicit prompt so the unattended run has no room to wander:
When that one-shot behaves the way you want, you have your deployment: the same prompt, the same allow-list, the same key from the environment, now running on a host on a trigger you choose. Wrap it so the run is reproducible:
# build an image that contains your code, not your secret
docker build -t store-review-agent .
# run it, injecting the key at run time from your shell or CI secret
docker run --rm -e ANTHROPIC_API_KEY store-review-agentNotice the key is passed in at docker run, not built into the image. The image
is safe to push to a registry; the secret never leaves the run.Common gotchaThe -e ANTHROPIC_API_KEY form with no =value only forwards the variable if it is set in the calling shell or CI environment; when it is unset, Docker passes nothing and the container starts with an empty key, so the agent fails on its first model call with a confusing 401 rather than a clear "key missing" error.Verify the variable is exported in the run context first, or pass -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" and let the run fail loudly if it is blank.
Watch the run
Unattended does not mean unobserved. The agent loop emits a stream of structured messages: each step it takes, each tool it calls, and a final result that includes the outcome and the cost. Send that stream to your logs.
You read those logs for three things: did it finish, what did it touch, and what did it spend. The final result message carries token usage, which is how you keep an eye on cost before a runaway loop becomes an expensive surprise. A sensible first deployment also has a hard stop: a maximum number of turns or a wall-clock timeout, so a confused agent halts instead of looping forever.Common gotchaSet the turn limit before the first run, not after. On a laptop you notice a stuck loop and hit Ctrl-C; an unattended agent with no cap can spend hours and a large bill calling the model in circles before anyone checks the logs.Give it a low ceiling (say 15 turns) and raise it only once you have seen real runs finish under it.
A checklist before you ship
Run through this before the first unattended run:
- The key is injected at run time, not in the image, the YAML, or git.
-
allowedToolslists only what the task needs, nothing more. - No
bypassPermissionsoutside a sandbox; the run fails closed. - Every external system is reached through MCP, with its own injected secrets.
- There is a turn limit or timeout so the loop cannot run forever.
- The message stream goes to logs you will read.
- You tested the exact prompt and allow-list as a one-shot first.
Where to go next
A read-only reviewer is a real deployment and a safe place to learn the moving
parts. Once it runs cleanly, you widen it on purpose: add Edit for an agent
that proposes patches, add an MCP server for an agent that reads your database,
add a permission callback for the few decisions you want to make in code.
If you want stronger guardrails as you widen the blast radius, read hooks and guardrails for code that runs at fixed points in the loop. And if running your own host stops being worth it, that is the signal to look at managed agents, where the infrastructure becomes someone else's problem and you keep the prompt, the tools, and the rules.