AIAgents

Cloud Run finally adopted an insomniac

My AI agent hung up on me after exactly five minutes. It did not wait for four minutes, nor did it stretch to six. The line went dead at five minutes flat, displaying the sterile punctuality of a scheduled dental cleaning and the interpersonal warmth of a parking meter. I reconnected. Five minutes later, another hang-up. I reconnected yet again, driven by that specific primate delusion that makes humans violently mash an elevator button that is already glowing. Five minutes.

This is the clinical record of how that aggressively rude disconnection finally explained to me why Google issued a fourth child to the Cloud Run family, and why this particular infant absolutely refuses to go to bed.

A family that already reached maximum occupancy

Cloud Run is the designated quarantine zone where most of my experiments go to live, especially the ones involving artificial intelligence workloads. Therefore, when Google announced Cloud Run instances in preview, my initial reaction was not one of boundless joy. It was the haunted expression of an exhausted parent being told that yet another sibling is on the way. Cloud Run already possessed three execution models, and they seemed to cover every conceivable household chore:

  • Services, the aggressively sociable one. This sibling greets every single visitor, answers every request, and drops into a deep coma the exact millisecond the last guest leaves the living room. The marketing brochure politely refers to this narcolepsy as “scaling to zero.”
  • Jobs, the brooding teenager who shows up, runs a five-hour load of heavy laundry, and departs without saying goodbye or making eye contact.
  • Worker Pools, the subterranean dweller. It lives in the basement, quietly pulls tasks from a queue, and possesses no public URL. Nobody has its phone number. Nobody asks how its day went.

Why did we need a fourth? What bizarre task could this new arrival possibly execute that the other three could not handle between them?

(A brief warning about vocabulary before we proceed. Cloud Run already utilized the word “instance” for the container copies a Service spins up when it scales. Now there is a separate product officially named Cloud Run instances. This linguistic choice means the sentence “my instance ran out of instances” is now grammatically valid. Sooner or later someone will say this in a corporate meeting with a completely straight face. For our purposes, “Instance” with a capital I refers to the new product. When in doubt during your daily life, ask the speaker which instance they mean. Then ask them again just to be safe.)

The experiment that worked and explained absolutely nothing

My plan was to find the justification for this new product empirically. I would build an AI agent that runs long-lived sessions and keeps temporary data locally, deploy it as a regular Cloud Run Service, and watch it fail spectacularly. The failure would reveal the true purpose of Instances.

I mounted a Cloud Storage bucket as a volume and gave the agent a tool to read and write its session state there. I added an ephemeral disk for the scratch data it produced mid-task. Then I sat back and waited for the disaster.

Disaster did not come. The agent handled long sessions without complaint. It scaled to zero when ignored, costing me nothing, and when I called it later it picked up the exact same session as if it had just stepped out for a coffee. Scientifically speaking, this was the worst possible outcome. A total success without any comprehension of the underlying mechanics is just a failure with an excellent public relations team.

Of course, the setup was deeply flawed. Cloud Storage mounted through FUSE (Filesystem in Userspace) is less like a modern hard drive and more like trying to maintain a deep, philosophical conversation by sending telegrams via carrier pigeons with bad attitudes. The latency is palpable, and real POSIX file locking is completely off the menu. The ephemeral disk, meanwhile, was wiped completely clean every time the service scaled to zero. It was like hiring an overzealous sanitation team that incinerates your filing cabinets the moment you step out for a bathroom break.

But here is the catch. None of those problems justified the new Instances product, because an Instance would suffer from the exact same storage limitations. My confusion had metastasized into a peer-reviewed finding.

The waiter who violently confiscates your plate

The breakthrough arrived when I stopped thinking of agents as people who mail letters and started thinking of them as people who make phone calls.

Request and response is letter writing. A question goes out, an answer comes back, and the mailbox goes quiet. That is the natural habitat of a Cloud Run Service. But many long-running agents do not write letters. They open a WebSocket and keep it perpetually open, pushing a steady trickle of updates back to the user. That is a phone call meant to stay active for hours.

When I rebuilt my agent using this methodology, I met the five-minute hang-up from the first paragraph. The culprit was the request timeout. On a Cloud Run Service, a WebSocket connection is treated as a standard request, and every request has a deadline. You can stretch this maximum timeout to 60 minutes. To a long-running AI agent, sixty minutes is equivalent to a waiter staring unblinkingly into your eyes while ripping the plate from your hands and charging you for the privilege of not swallowing your food.

This is precisely the gap Cloud Run instances fill. Unlike Cloud Run services that scale based on incoming traffic, an Instance is a dedicated compute container managed manually through lifecycle transitions (create, stop, start, update, delete). It gets an HTTPS address that survives updates and restarts. It holds connections open. It does not suffer from narcolepsy when traffic stops. It is the insomniac cousin who always picks up the phone at three in the morning.

Creating one requires a single beta command. Just remember to keep IAM invoker authentication enabled. Bypassing IAM is fine for a quick demo, but for an AI agent with access to your calendar and inbox, you probably do not want it casually chatting with the entire internet.

What the insomniac actually does for a living

Long-running AI agents got me through the door, but they are not the only tenants suited for this environment.

Personal agents and workflow engines. If I run an automation tool like n8n or a personal agent for myself, I do not need it to scale to a thousand users. I need it awake, holding its state, and reachable at a stable URL that refuses to hang up after an hour. This is the flagship use case.

A quick bastion host. Giving developers access to a private database usually requires provisioning a Virtual Machine and babysitting SSH keys, which is the IT equivalent of building a dedicated two-story garage just to store a single garden rake. Now, developers can use Instances as bastion hosts to reach internal resources. Just check the current documentation, because SSH access for this preview product changes rapidly.

Sandboxes and code playgrounds. Sometimes you just need an addressable box to execute code you do not entirely trust, a playground you fully intend to delete next week. Pair an Instance with Cloud Run sandboxes, and you get that environment without standing up a massive Kubernetes cluster.

The argument for Instances is not raw capability. A dedicated Virtual Machine can do all of this. The argument is convenience and price. Running an Instance with 1 vCPU and 1 GiB of memory continuously for a month costs around $5.70. Unlike a vending machine sandwich of the exact same price, this compute container will not give you severe heartburn, though its absolute lack of local persistent storage might cause a mild nervous breakdown. The insomniac does its own laundry, saving you from patching operating systems or provisioning HTTPS endpoints.

The fine print, read aloud in a clinical setting

Every adoption comes with paperwork. Here is the part of the file the agency hoped you would not read.

It is a goldfish. An Instance is a singleton with zero autoscaling. It is cheap and low maintenance, but if you overfeed it, it floats belly-up. When your app gets a sudden spike in traffic, Cloud Run will not spin up a helpful sibling. Requests will queue politely until they quietly die of old age. Internet users love to tap on the server glass until the poor fish suffers a fatal HTTP-induced collapse. Keep IAM authentication turned on so strangers cannot overfeed a goldfish they cannot reach.

It has nowhere safe to keep its things. An Instance feels like a VM, but it has no local persistent disk. Anything written to the temporary folder lives in the container’s memory, so your scratch files compete directly with your application for RAM. It is like storing your weekly groceries in your jacket pockets.

It suffers from weekly amnesia. Instances can stay active for up to seven days before a mandatory restart policy kicks in. When that happens, everything in memory and on the ephemeral disk is instantly vaporized. Think of a corporate office worker who suffers a blunt-force head trauma every Sunday at midnight. They show up on Monday morning smiling, with their tie on backward, having absolutely no recollection of their own name, waiting for an external database to explain who they are. Anything that must survive this reboot belongs in a bucket.

It is still in preview. Running production workloads on a preview product is not strictly forbidden. It is just the infrastructure version of moving your living room furniture into a house while the construction crew is actively sawing the legs off the staircase.

A Field Guide to the Cloud Run Household

By the end of my investigation, the entire Cloud Run family finally made clinical sense. Since tables tend to break when viewed on mobile phones, think of this as a highly practical survival guide for your next architectural decision:

  • The Service: Reach for this when you need a web app or API that scales massively with traffic and takes a nap when idle.
  • The Job: Reach for this when you have a batch task that runs for hours and then permanently clocks out.
  • The Worker Pool: Reach for this when you need background workers pulling from a queue, hiding safely from the public internet.
  • The Instance: Reach for this when you need one always-on container with a stable URL that holds connections for days without hanging up.

The first three members of the family were built around the very reasonable idea that you should not pay for a machine that is not actively working. The new sibling is built around a different, equally reasonable idea: some things are only useful if they are completely awake when you call them.

So yes, Cloud Run adopted an insomniac. It costs about as much as a sad sandwich, it answers the phone at any hour, it violently forgets its own identity once a week, and it will panic if more than a handful of people speak to it simultaneously. I have worked with human beings exactly like that. Some of them were excellent colleagues.

Just remember, when you tell your team you are “moving the agent to an instance,” clarify exactly which instance you mean. Then clarify it again, just to be safe.

Securing Hermes Agent without losing your mind in the process

I spent an evening reading the source of an AI agent that had been running on my own machine for three weeks, and I came away with two feelings that do not normally coexist. The first was relief, because the people at Nous Research clearly thought about this harder than I expected. The second was a mild, creeping unease, because the parts they could not protect are exactly the parts I had been ignoring.

Hermes Agent is an autonomous agent with persistent memory. It keeps state across sessions, works through long goals on its own schedule, writes its own reusable skills from experience, and talks to you from Telegram, Discord, or Slack while it does it. That last detail is the one that changes everything. A coding assistant sits politely in your IDE waiting to be asked. Hermes runs on a VPS you are not looking at, at three in the morning, and reports back later.

That is the feature. It is also the problem. So this is a guide to putting Hermes somewhere useful without handing it the keys to your production environment, written after actually reading what it already does for you, which turns out to be more than most blog posts on this subject assume.

Why an always-on agent is a different animal

The security model of a chatbot is simple because a chatbot has no initiative. It answers, it stops, it waits. Nothing happens between your messages.

An autonomous agent inverts that. Hermes monitors, decides, and acts without a human in the loop, which means three properties collapse together in a way that traditional threat modelling does not handle well.

It has initiative, so the trigger for an action may be a cron job or a Slack message from someone who is not you. It has memory, so a decision it makes today can influence a decision it makes next month, long after you have forgotten the context. And it has tools, so its output is not text; it is a shell command, an API call, a kubectl apply.

Combine those, and you get a category of failure that does not exist in ordinary software. An attacker does not need to compromise the agent’s process. They only need to get some text in front of it. A poisoned README in a repo it clones, a crafted issue on GitHub, a message in a channel it monitors. Prompt injection is not a memory safety bug you can patch. It is a consequence of the agent doing its job, which is reading things and acting on them.

What Hermes already gives you, which is not nothing

Here is the part that most security write-ups skip, and skipping it makes them both unfair and less useful. Hermes ships a documented defence-in-depth model with eight layers, and if you deploy it without knowing what they are, you will end up rebuilding controls that already exist while leaving the real gaps open.

The ones worth knowing before you write a single line of infrastructure:

Dangerous command approval. Before running a shell command, Hermes matches it against a list of destructive patterns (rm -r, mkfs, dd if=, DROP TABLE, curl … | sh, writes to /etc/ or ~/.ssh/). The default smart mode uses an auxiliary model to triage, trivially safe commands pass, clearly dangerous ones are denied, ambiguous ones escalate to you. Approval prompts fail closed after a timeout.

A hardline blocklist underneath all of it. A handful of unrecoverable commands (rm -rf /, fork bombs, zeroing a block device) are refused regardless of –yolo, regardless of “approvals.mode: off”, regardless of you clicking “allow always”. There is no override flag. This is a genuinely good design decision, and I wish more tools had it.

File write safety. write_file and patch are blocked from touching credential stores (~/.ssh/, ~/.aws/, ~/.kube/, .env files anywhere on disk) with no approval prompt and no way to override from chat.

SSRF protection on every URL-capable tool. Private ranges, loopback, link-local (including 169.254.169.254, the cloud metadata endpoint), and cloud metadata hostnames are blocked by default, with redirect chains revalidated at each hop.

Context file injection scanning. AGENTS.md, .cursorrules, and similar files are scanned for injection patterns, hidden HTML comments, and invisible Unicode before they reach the system prompt.

Gateway authorization that defaults to deny. If you configure no allowlists, nobody can talk to the bot.

Now, the important caveat, which the documentation itself states plainly. The write guards apply only to write_file and patch. The terminal tool runs as the same OS user and can cat or overwrite those same paths with a shell command. The approval system is a guardrail against an honest-but-mistaken agent. It is explicitly not a sandbox against a hostile one.

That distinction is the whole reason the rest of this article exists. Everything above stops the agent from making a mistake. Almost none of it stops an agent that has been successfully talked into something.

What “secure” should mean here

Before the configuration, the goals. An agent deployment is defensible when five things are true.

It has its own identity. The agent acts as itself, never as you. Every action is attributable to a principal that exists only for the agent and dies with it.

It runs least privilege by default-deny. It reaches exactly the systems its job requires, and the list of those systems is written down somewhere reviewable.

Its credentials are short-lived, narrow, and ideally invisible to it. The best secret is one the agent never holds.

Its runtime is contained. A compromised agent stays a compromised agent instead of becoming a compromised host.

Everything it does is reconstructable from immutable logs. Not from asking the agent what it remembers doing, which is roughly as reliable as asking a witness.

And a sixth one, specific to Hermes and to any agent with a learning loop, which I did not appreciate until I read the skills documentation: what the agent learns is code, and it must be treated as code. More on that in step seven, which is the step I would keep if I could only keep one.

Step 1: Contain the runtime

Never run the agent on the host, and never as root. Hermes makes this a one-line decision because the terminal backend is configurable, and switching it to Docker moves execution into a container that Hermes hardens itself:

# ~/.hermes/config.yaml

terminal:

  backend: docker

  docker_image: "nikolaik/python-nodejs:python3.11-nodejs20"

  docker_forward_env: []      # explicit allowlist only, empty keeps secrets out

  container_cpu: 1

  container_memory: 2048      # MB

  container_disk: 20480       # MB

  container_persistent: false # fresh filesystem per session

Every container Hermes launches gets –cap-drop ALL (with DAC_OVERRIDE, CHOWN and FOWNER added back so package managers work), –security-opt no-new-privileges, a 256 process limit, and size-limited tmpfs mounts on /tmp and /var/tmp with noexec on the latter. That is a better default than most hand-rolled docker run lines I have reviewed in production, including some of mine.

Two things to know about this switch.

First, “container_persistent: false” is the setting people skip. In persistent mode, the sandbox filesystem survives across sessions, which means an attacker who lands something in /workspace on Monday still has it on Thursday. Ephemeral mode throws it away. Use ephemeral unless you have a concrete reason not to.

Second, and this one surprised me. When the backend is a container, Hermes skips the dangerous command checks entirely, on the reasoning that the container is now the boundary. That reasoning is correct, and it also means your blast radius is now exactly the container definition. If you bind-mount your home directory in, you have quietly deleted both layers at once.

If you want a real boundary instead of a shared kernel, run this inside a microVM. Firecracker or Cloud Hypervisor boots in tens of milliseconds and gives you a hardware isolation line, which is a proportionate response to a workload whose behaviour you cannot fully predict.

If you use the official Docker image, note the operational trap. The gateway runs as the unprivileged hermes user (uid 10000), but “docker exec” defaults to root, and files that root creates are unreadable to the gateway. Pairing approvals fail silently.

docker exec -u hermes hermes-agent hermes pairing approve telegram ABC12DEF

Step 2: Control the egress

Data exfiltration is the worst outcome of a successful prompt injection, and it is the one where network controls beat application controls decisively. The agent can be talked into anything. The firewall cannot.

Start with the two settings Hermes already exposes:

# ~/.hermes/config.yaml

security:

  allow_private_urls: false     # default, keep it that way on any gateway

  website_blocklist:

    enabled: true

    domains:

      - "*.internal.company.com"

      - "admin.example.com"

  tirith_enabled: true

  tirith_fail_open: false       # block when the scanner is unavailable

  allow_lazy_installs: false    # no runtime pip installs

“tirith_fail_open: false” is the change worth arguing about. The default is true, meaning commands proceed if the content scanner is missing or times out. That is the right default for a laptop and the wrong one for a production gateway, where a scanner that is not running should stop the line rather than wave things through.

Then put a real allowlist under it, at the network layer, where the agent’s opinions do not matter. On Kubernetes:

apiVersion: networking.k8s.io/v1

kind: NetworkPolicy

metadata:

  name: hermes-agent-egress

  namespace: agents

spec:

  podSelector:

    matchLabels:

      app: hermes-agent

  policyTypes:

    - Egress

  egress:

    # DNS only to the cluster resolver

    - to:

        - namespaceSelector:

            matchLabels:

              kubernetes.io/metadata.name: kube-system

          podSelector:

            matchLabels:

              k8s-app: kube-dns

      ports:

        - protocol: UDP

          port: 53

    # everything else goes through the proxy, nowhere else

    - to:

        - podSelector:

            matchLabels:

              app: egress-proxy

      ports:

        - protocol: TCP

          port: 3128

Nothing else leaves. When the injection eventually happens, and it will, the exfiltration attempt dies at the network layer and lands in your proxy logs, which is the best possible outcome. An attack that failed and told you about itself.

Step 3: Give the agent its own identity

If the agent uses your kubeconfig, the agent is you. On a bad day, that means it holds cluster admin, and every command it hallucinates is permanently attributed to your name in the audit log. Explaining that in a post-incident review is a specific kind of misery.

Give it a ServiceAccount scoped to the handful of verbs it actually needs:

apiVersion: v1

kind: ServiceAccount

metadata:

  name: hermes-agent

  namespace: agents

---

apiVersion: rbac.authorization.k8s.io/v1

kind: Role

metadata:

  name: hermes-agent-reader

  namespace: apps

rules:

  - apiGroups: [""]

    resources: ["pods", "pods/log", "events", "services"]

    verbs: ["get", "list", "watch"]

  - apiGroups: ["apps"]

    resources: ["deployments", "replicasets"]

    verbs: ["get", "list", "watch"]

---

apiVersion: rbac.authorization.k8s.io/v1

kind: RoleBinding

metadata:

  name: hermes-agent-reader

  namespace: apps

subjects:

  - kind: ServiceAccount

    name: hermes-agent

    namespace: agents

roleRef:

  kind: Role

  name: hermes-agent-reader

  apiGroup: rbac.authorization.k8s.io

Read-only, namespaced, no wildcards. When the agent needs to restart a deployment, resist the urge to add patch on deployments and instead give it one narrow verb on one named resource, or better, a pipeline it can trigger that a human owns. Every verb you add here is a verb an attacker inherits.

Apply the same paranoia everywhere else it touches: a dedicated GitHub App with repository-scoped permissions instead of your PAT, a dedicated cloud service account instead of your admin role.

Step 4: Keep credentials short-lived, or absent

Long-lived static credentials are a bad idea in ordinary software. Handed to an agent that can be talked into printing them, they are a liability with an expiry date you do not control.

The first discipline is passthrough hygiene. Hermes strips sensitive variables from child processes by default: execute_code blocks anything whose name contains KEY, TOKEN, SECRET, PASSWORD, CREDENTIAL, or AUTH, and MCP subprocesses receive only PATH, HOME, USER, LANG, LC_ALL, TERM, SHELL, TMPDIR, and XDG_*. Everything else is stripped. Do not undo this. Every name you add to docker_forward_env or terminal.env_passthrough is a secret that code in the container can read and send anywhere.

The second is to stop giving it the secret at all. This is where I have to correct something I believed when I started writing: I assumed you would have to build the credential-injection proxy yourself as a sidecar. You do not. Hermes ships one.

hermes egress setup

The egress proxy (iron-proxy, a TLS-intercepting single binary managed by the Hermes egress commands) holds your real API keys on the host and gives the sandbox nothing but opaque tokens. The agent asks the proxy to make the call. The proxy injects the credential on the way out. The sandbox never sees a usable secret, so an injection that convinces the agent to exfiltrate its credentials exfiltrates a token that is worthless outside the proxy.

This is the single highest-value control in the entire article. It takes one command, and it is documented in a corner of the docs that almost nobody reads. If you take one thing from this piece, take this.

For cloud access, the same principle applies through Workload Identity or IRSA. The pod’s identity is federated at the API boundary, and there is no key material on disk to steal.

Step 5: Build an audit trail you can actually query

You need to answer who did what and when, from logs the agent cannot edit. Three sources, aggregated centrally:

The proxy access log, which is your ground truth for every outbound request, including the ones that were blocked.

The Kubernetes API server audit log, filtered to the agent’s identity so it is readable:

apiVersion: audit.k8s.io/v1

kind: Policy

rules:

  - level: RequestResponse

    users: ["system:serviceaccount:agents:hermes-agent"]

  - level: Metadata

    resources:

      - group: ""

        resources: ["secrets", "configmaps"]

And Hermes’ own state, which lives in ~/.hermes/logs/ and ~/.hermes/state.db. That database is genuinely useful, because it records which dangerous commands were classified and which ones actually executed. There is even a command that mines it:

hermes approvals suggest --days 90

It prints the patterns you approved most often. Read it as a confession rather than a convenience: if you have approved git push –force fourteen times, you have not been reviewing those prompts. You have been dismissing them. Ship ~/.hermes/ to your SIEM on a schedule, and remember that these logs live inside the blast radius, so they corroborate the external ones rather than replacing them.

Step 6: Cap the blast radius of always-on

Always-on means the exposure window never closes, so put ceilings on everything that can run away.

# ~/.hermes/config.yaml

approvals:

  mode: manual          # no auxiliary-model triage in production

  timeout: 120

  cron_mode: deny       # headless jobs never auto-approve

  single_query_mode: deny

  deny:

    - "git push --force*"

    - "kubectl delete*"

    - "terraform apply*"

    - "*curl*|*sh*"

Note what approvals.deny is for. It sits below –yolo and “approvals.mode: off”, so it survives the moment six months from now when somebody adds –yolo to a script to unblock a deploy. Write the list for that person, because that person is you on a Friday.

Set a hard spending cap on the provider API key at the provider. And keep the gateway allowlist explicit. Never “GATEWAY_ALLOW_ALL_USERS=true”:

# ~/.hermes/.env

TELEGRAM_ALLOWED_USERS=123456789

SLACK_ALLOWED_USERS=U01ABC123

chmod 600 ~/.hermes/.env

Step 7: Treat what the agent learns as untrusted code

This is the step that does not appear in generic agent hardening guides, because it is specific to agents that learn, and it is the one I would fight to keep.

Hermes’ defining feature is its learning loop. When it solves something, it writes a reusable skill as a Markdown file, stores the outcome in persistent memory, and adjusts next time. Agent-created skills land in ~/.hermes/skills/.

Sit with that for a second. The agent writes procedure documents that the agent later follows. Which means a prompt injection does not have to steal anything today. It can instead persuade the agent to write a skill, and that skill will be loaded and followed next week, next month, in a session that has nothing to do with the original attack, triggered by a cron job while you are asleep. Every control in steps one through six is scoped to a session. This one crosses sessions. It is persistence, in the red-team sense of the word, implemented as a feature.

Nous clearly thought about this. Skills installed from the Hub and skills carried by repositories are scanned for prompt injection directives, credential exfiltration commands, and hidden text tricks, and a skill that fails the scan is quarantined so it does not appear in the index and refuses to load by name. Repository skills require an explicit “hermes skills trust” before they load at all.

But a scanner is a filter, and filters have false negatives. For anything touching production, turn the gates on:

# ~/.hermes/config.yaml

skills:

  write_approval: true    # every skill create/edit/delete waits for you

memory:

  memory_enabled: true

  write_approval: true    # same gate on memory writes

With these on, writes are staged under ~/.hermes/pending/skills/ and you review them like a pull request:

/skills pending

/skills diff <id>

/skills approve <id>

/skills reject <id>

Then go one step further and make the skills directory a git repository:

cd ~/.hermes/skills && git init && git add -A

git commit -m "baseline: approved skill set"

Now every change the agent proposes to its own behaviour produces a diff with a timestamp and an author, reviewed by a human, revertable with one command. This costs you a few minutes a week and converts the most alarming property of the agent into the most auditable one.

One more thing on state. If you use the SSH, Modal, or Daytona backends, Hermes pushes ~/.hermes/ into the remote sandbox and syncs changed files back to the host afterwards, including skills the agent created remotely. The sandbox boundary you carefully built is, for this specific directory, a two-way street. Plan accordingly.

What is still broken after all seven steps

Two things, and I would rather say them than pretend the checklist is complete.

The terminal tool remains a hole in the write guards. Hermes’ protected-path denylist stops write_file and patch from touching ~/.ssh/ or .env files, but the terminal tool runs as the same OS user and can cat them with a shell command. The documentation says so explicitly. The only real answer is the container or microVM boundary from step one, which is why step one is step one.

Your guardrails now live in five different places. Kubernetes RBAC, cloud IAM, a NetworkPolicy, a proxy allowlist, and a YAML file in a home directory. There is no single pane of glass showing what the agent can do, and no way to ask “can it reach the payments database?” without checking five systems and reasoning about their intersection. Until unified agent control planes exist, the answer is Terraform: put all five in one repository, in one module, reviewed together, so that at least the drift is visible.

module "hermes_agent" {

  source = "./modules/agent-sandbox"

  agent_name          = "hermes-prod"

  k8s_namespace       = "agents"

  allowed_egress_fqdn = ["api.github.com", "hooks.slack.com"]

  iam_role_arn        = aws_iam_role.hermes_scoped.arn

  spend_cap_usd       = 200

}

The bottom line

Hermes Agent is a serious piece of engineering, and after a week of reading its source, I trust it more than I did going in, not less. It will automate the work you have been putting off, run deployments while you sleep, and behave, most of the time, like the relentless junior engineer you never managed to hire.

The thing to internalise is that its defaults are tuned for a developer laptop, which is the correct choice for the audience it has. Production is a different audience, and the gap between those two configurations is roughly the seven steps above. None of it is exotic. It is a container backend, a network policy, a service account, one command to set up the egress proxy, some log shipping, a deny list, and a git repository for the skills directory.

Give Hermes a well-lit room with a door you control, and it will change how you work. Give it your kubeconfig and an open egress path, and it will also change how you work, though the meeting where you explain it will be considerably less pleasant.