Running an autonomous AI agent on a VPS with claude -p (and why Bedrock in production) | OptimyCloud

Running an AI agent autonomously on a VPS — and why I move to Bedrock in production

July 22, 2026 12 min read Alexandre Gillon
Server rack hosting an autonomous AI agent

Putting an AI agent on a VPS takes about twenty minutes. One apt install, an API key in a .env file, a cron entry, and you have a machine that triages your logs every night. It genuinely works, and it feels good.

The trouble comes later: the day that setup stops running on your own server and starts running at a client's, with their data, their bill, and their IT lead asking questions. At that point the API key in the .env becomes a real problem, and there is no elegant patch for it.

This article does both: the setup that works, then the version I actually deploy in production, built on Amazon Bedrock. Switching from one to the other comes down to a single environment variable — that is all that separates a side project from an auditable piece of infrastructure.

The basic setup: claude -p on a VPS

Claude Code is not only an interactive terminal interface. The -p flag (or --print) switches it into non-interactive mode: it reads standard input, writes to standard output and returns an exit code. In other words it behaves like grep or jq, and can be driven from any shell script.

That is what makes the agent deployable: no session, no TTY, no lingering tmux. An entry-level VPS is enough — inference runs on the API side, the machine only orchestrates.

# Install and first non-interactive call

curl -fsSL https://claude.ai/install.sh | bash

export ANTHROPIC_API_KEY="sk-ant-..."

claude -p "Summarize the errors in this log" < /var/log/app/error.log

The four flags that matter

  • --bare — disables auto-discovery of hooks, skills, plugins, MCP servers and CLAUDE.md files. Without it, your agent loads whatever happens to sit in the working directory and in ~/.claude. In production this is the flag that guarantees the command does the same thing on every machine.
  • --output-format json — returns a structured object instead of free text: the result, the session ID, token usage and the cost of the run. That is what makes orchestration possible: parse it with jq and branch on the output.
  • --allowedTools — the allowlist of tools the agent may use without asking. The syntax supports prefix matching: Bash(git diff *) allows any command starting with git diff. The space before the asterisk is significant.
  • --append-system-prompt — adds instructions to the system prompt without replacing default behavior. This is where you define the agent's role: security reviewer, incident triager, report writer.

# A review agent driven by a systemd timer

#!/usr/bin/env bash
set -euo pipefail

cd /srv/project

git diff origin/main | claude --bare -p \
  --append-system-prompt "You are a security engineer. Report only
  exploitable vulnerabilities, with file and line." \
  --output-format json \
  --allowedTools "Read" \
  | jq -r '.result' > /var/reports/review-$(date +%F).md

A systemd timer, a cron entry, and you have an agent producing a review report every morning. It is genuinely useful, and it costs a few cents per run.

One detail that saves a bad surprise: piped standard input is capped at 10 MB. Past that, the command exits with an error and a non-zero status. For larger volumes, write the content to a file and reference the path in the prompt instead of piping it.

What breaks the moment it stops being a side project

The setup above is perfectly viable on your own server. It becomes hard to defend the day it runs at a client's. Four things block, and none of them is solved by tidier file layout.

The API key is a static secret, in plain text, on the machine

It never rotates on its own, it belongs to a named account, and it lives in a file the process can read. An agent that runs shell commands runs, by construction, on the same machine as the secret paying for its calls. The day someone leaves the company or a backup leaks, you revoke by hand and redeploy everywhere.

No usable audit trail

You know how much the key consumed in total. You do not know which service, which machine, which job. In an audit, "an agent calls an API with a shared key" is not an acceptable answer: you will be asked who called what, when, and under which authorization.

No technical spending cap

An agent looping on a poorly scoped task keeps spending until somebody notices. The only native guardrail on an API key is the credit card limit behind it — plus an overage alert that arrives after the fact. On a client budget, that is not enough. We covered cloud spend control in our FinOps audit guide.

The data question always comes up

As soon as an agent reads business files, application logs or proprietary code, the client's first question is where that data travels and under which jurisdiction it is processed. It is a fair question, and it is better answered with an architecture than with a screenshot of someone's terms of service.

The production version: the same agent, through Amazon Bedrock

Amazon Bedrock serves Claude models from inside an AWS account. For the agent, nothing changes: same commands, same tools, same prompt. What changes is everything around the call.

# The complete switch

export CLAUDE_CODE_USE_BEDROCK=1
export AWS_REGION=eu-west-3

# Model pinning (strongly recommended)
export ANTHROPIC_MODEL='eu.anthropic.claude-sonnet-4-5-20250929-v1:0'

claude --bare -p "Summarize the errors in this log" < /var/log/app/error.log

There is no ANTHROPIC_API_KEY in the environment anymore. Claude Code uses the standard AWS SDK credential chain: on an EC2 instance it picks up temporary credentials from the IAM role attached to the machine. No secret is written to disk, and rotation is handled by AWS.

The minimal IAM policy

This is the heart of it: the agent's permissions become a declarative resource, versioned in your Terraform, reviewed in a pull request.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "bedrock:InvokeModel",
      "bedrock:InvokeModelWithResponseStream",
      "bedrock:ListInferenceProfiles",
      "bedrock:GetInferenceProfile"
    ],
    "Resource": [
      "arn:aws:bedrock:*:*:inference-profile/*",
      "arn:aws:bedrock:*:*:foundation-model/*"
    ]
  }]
}

Narrow the resource down to the ARNs of the inference profiles the agent is actually allowed to call and you get a clean perimeter: this agent, on these models, in this account.

What that changes in practice

Per-call traceability

Every invocation goes through CloudTrail: which IAM principal, from which instance, on which model, at what time. That is the difference between "we think it was the nightly job" and a sourced answer in thirty seconds.

Budget and chargeback

Consumption shows up in Cost Explorer like any other AWS service. You apply tags, charge back per team or project, and wire AWS Budgets to an alert at a threshold you choose. Bedrock service tiers (default, flex, priority) also let you trade cost against latency on background work.

Data residency

Bedrock exposes cross-region inference profiles prefixed with eu. that route only to European regions. One caveat: availability varies by model, and some recent models are offered in Europe only through a Global profile that routes across all commercial regions. Check before you commit.

Centralized content filtering

Bedrock Guardrails is applied via request headers, so it sits outside the agent's code. A security team can enforce a filtering policy without touching the deployed scripts.

# Check what is actually available in your region

aws bedrock list-inference-profiles --region eu-west-3

Two limitations to know before switching.

The native web search tool is not available on Amazon Bedrock. If your agent needs to consult online sources, you have to go through an MCP server or a custom tool. This is the point that surprises people most during migration.

Prompt caching is not enabled in every Bedrock region. An agent that resends a large context on every run pays full price when the cache is unavailable — watch the cached-token counters, and if they stay at zero the region does not support it.

Pin your models

Without explicit pinning, the sonnet and opus aliases resolve to Claude Code's built-in defaults, which shift across versions and may not be enabled in the client's account. Across a fleet of machines, that guarantees behavior changing on its own one morning — and the bill changing with it, since an Opus model costs noticeably more than a Sonnet.

export ANTHROPIC_DEFAULT_SONNET_MODEL='eu.anthropic.claude-sonnet-4-5-20250929-v1:0'
export ANTHROPIC_DEFAULT_HAIKU_MODEL='eu.anthropic.claude-haiku-4-5-20251001-v1:0'

In an enterprise setting you go further: application inference profiles let you route each model version to an ARN managed by the organization, with its own quotas and its own cost tracking.

What if the client isn't on AWS?

The argument in this article is not "use Bedrock". It is that a production agent must not carry a static secret, and must be traceable, capped, and controlled as to where its data is processed. Bedrock is one way to get those four properties when the client is already on AWS. It is not the only one.

On GCP, Azure or a mixed estate, the usual answer is an LLM gateway — LiteLLM being the most widely deployed open-source implementation. It sits between the agent and the provider and delivers exactly the same thing: the provider key stays server-side, each service or developer gets its own credential, usage is attributed, budgets and rate limits are enforced in one place, and every request is logged.

On the agent side, the switch is once again a single variable:

export ANTHROPIC_BASE_URL=https://gateway.internal.example.com

A gateway even brings one property Bedrock does not: switching provider happens in the gateway's configuration, without touching the machines running the agents.

The trade-off is real, and worth stating before you commit: the gateway becomes infrastructure you operate. It has to keep pace with the client — a gateway that fails to forward a new capability breaks the corresponding feature. And Anthropic does not maintain or audit third-party gateway products.

Before you let it run: what to lock down

An autonomous agent on an exposed machine is a process running shell commands chosen by a model, based on content it reads. That content — a log, a GitHub issue, a file dropped by a third party — is not trusted. Five rules, in order of importance:

  • No --dangerously-skip-permissions on an exposed machine. The right setting is an allowlist via --allowedTools, optionally combined with the dontAsk mode, which denies anything not covered by your allow rules or the read-only command set. An agent that fails because it lacks a permission is a useful signal; an agent with every permission emits none.
  • A dedicated, unprivileged system user. No root, no shared deploy account. The agent only reaches the directories it must work on, with an IAM role carrying just the Bedrock actions it needs — and certainly not the account's administrative credentials.
  • Restricted network egress. An outbound security group limited to what the agent actually needs. That is the net that limits damage if malicious content manages to influence a command.
  • An execution time cap. A TimeoutStartSec on the systemd side, or a timeout around the call. On SIGTERM, Claude Code aborts the in-progress turn, terminates the process tree of any running command and exits with code 143 — the shutdown is clean, but something still has to trigger it.
  • Retained logs. Redirect the JSON output to a file or to CloudWatch. The day the agent does something unexpected, the full session trace is the only thing that lets you understand it.

API key or Bedrock: the table

Criterion API key Amazon Bedrock
Authentication Static secret on disk IAM role, temporary credentials
Rotation Manual Automatic
Traceability Total volume per key CloudTrail, per call
Budget control After-the-fact alert AWS Budgets, quotas, tags
Data residency Provider-dependent eu. profiles (check per model)
Native web search Available Not available
Setup effort A few minutes AWS account, IAM, model access

The verdict is fairly simple. For prototyping, exploring, automating your own tasks: the API key, without hesitation — it is faster and the governance overhead is not justified. As soon as there is a client, data that is not yours, or a budget to defend: a governed architecture — Bedrock if the client is already on AWS, an LLM gateway otherwise. Either way, the move happens without rewriting a single line of the agent.

Frequently asked questions

How big does the VPS need to be?

Much smaller than people expect. Inference runs at the model provider, not on your machine: the VPS orchestrates, reads files and runs commands. Two vCPUs and 2 GB of RAM cover most agents. Size for what the agent actually does — if it compiles or runs a test suite, that workload dictates the machine.

How do you chain several steps in the same context?

Non-interactive mode keeps sessions. Capture the session ID from the JSON output, then pass it back with --resume to continue the same conversation. --continue resumes the most recent one. Both commands must run from the same project directory.

How do you monitor the cost of a continuously running agent?

The --output-format json payload contains the total cost of the run and a per-model breakdown, so you can push it into a metric on every pass. On Bedrock, consumption also lands in Cost Explorer and is governed by AWS Budgets like any other AWS resource.

Do you really need a VPS, or is a Lambda enough?

It depends on duration. A Lambda works for an agent that answers within tens of seconds; for longer tasks an ECS Fargate container triggered by EventBridge fits better and removes the machine to maintain. The VPS stays simplest when the agent needs a persistent workspace — an already-cloned Git repository, a dependency cache.

What happens if the pinned model is not enabled on the AWS account?

Claude Code checks at startup that the models it intends to use are accessible. If the default model is unavailable and no pin is configured, it falls back to an earlier version for the current session and shows a notice — the fallback is not persisted. Avoiding exactly that silent behavior is why you pin explicitly in production.

Is Bedrock the only AWS option?

No. AWS also exposes an endpoint called Mantle, which serves Claude models in the native Anthropic API shape rather than the Bedrock Invoke shape, using the same AWS credentials. Enable it with CLAUDE_CODE_USE_MANTLE=1; it uses its own model IDs. Its catalog is separate from Bedrock's, and the two can coexist in the same session.

An AI agent to put into production?

Architecture, IAM roles, model pinning, budget control and execution hardening. I design and deploy autonomous AI agents on AWS, with the governance that goes with them.

Let's talk about your project

Reply within 24h - First conversation free and with no commitment