You can run Hermes Agent on a $5 VPS. You can run it on a GPU cluster. You can run it serverless on Daytona or Modal so it hibernates when idle and costs almost nothing between sessions. You can talk to it from Telegram while it works on a cloud VM. That flexibility is the headline feature - and it's also the security question this post is about.
NousResearch publishes a detailed security policy for Hermes Agent. It is unusually clear about what the project treats as load-bearing and what it does not. If you operate Hermes in the cloud, read it first; this post is the operator-friendly companion, not a replacement.
One line to hold onto while you read: containment decides how much damage a compromised agent can do, and visibility decides whether you ever know what it did. Hermes' policy is unusually clear about the first - the OS is the boundary. The second it leaves largely to the operator: Hermes writes activity logs to ~/.hermes/logs/, and deciding what gets inspected, kept, and reviewed is your call. This guide covers both.
What Hermes already gives you
Three things in the box are worth knowing about up front, because they shape the rest of the deployment story.
1. A trust model that names the boundary. Hermes Agent's policy says, in so many words: the only security boundary against an adversarial LLM is the operating system. Not the approval gate. Not output redaction. Not the Skills Guard. Those are useful - they catch the cooperative-mode mistakes that account for most real-world incidents - but they are heuristics operating on an attacker-influenced string, and the project does not pretend they are containment.
That's an honest framing, and it determines what "safely" means: you are responsible for choosing an OS-level isolation posture that matches the trust you've extended to the content flowing through the agent.
Worth separating two questions that often get blurred. Containment asks what a compromised agent can damage - and on that axis the policy is right: the OS is the boundary. Visibility asks something the policy doesn't address: what the agent actually did inside the boundary - what it touched, what it cost, what left through a channel you allowed. Containment can be perfect and you can still be blind on the second axis. The rest of this guide covers containment first, because it's load-bearing, then comes back to visibility.
2. Seven terminal backends. The terminal() tool - the one through which shell commands run - is pluggable. The supported backends are local, docker, ssh, singularity, modal, daytona, and vercel_sandbox. Switching from the default local backend to a containerized or remote one moves the agent's shell out of your host. Pick deliberately; the default does not isolate anything.
3. A non-trivial gateway. Hermes can be talked to from Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, SMS, and a dozen other platforms. Each adapter is a network-exposed surface. Each one needs an allowlist. The project's policy treats every adapter without an allowlist as a code bug, but operator configuration is required - adapters do not magically lock themselves to your account.
With those three points in mind, the rest of this guide is the practical follow-through.
Step 1 - Pick your isolation posture
Hermes Agent's policy describes two postures. They protect against different things, and operators should choose one explicitly rather than fall into the default.
Terminal-backend isolation swaps the local backend for a sandboxed one - Docker, an SSH'd container, Modal, Daytona, Vercel Sandbox. Shell and file tools (terminal, read_file, write_file, patch) execute inside the sandbox. What this confines: anything the agent does through the shell contract. What it does not confine: the code-execution tool (which spawns a host subprocess), MCP server subprocesses, plugins, hooks, and skill loading. Those are all in the agent's own Python process.
Whole-process wrapping runs the entire agent process tree - shell, code-execution, MCP, plugins, hooks, skills - inside a single sandbox. Hermes supports this two ways:
- The project's own Docker image and Compose setup. Lighter weight; standard container with operator-configured mounts and network policy.
- NVIDIA OpenShell. Per-session sandboxes with declarative policy across filesystem, network L7 egress, process/syscall layers, and inference routing. Credentials live in a Provider store and never touch the sandbox filesystem.
The decision rule from the policy: if the content flowing into the agent comes from surfaces you do not control - the open web, inbound email, multi-user Slack channels, untrusted MCP servers - you want whole-process wrapping. If the operator is trusted and the concern is just LLM-emitted destructive shell, terminal-backend isolation is the supported posture.
In practice: most cloud deployments fall into the first category. If your Hermes can be messaged from Telegram, you are accepting input from a surface you don't fully control. Plan accordingly.
Step 2 - Lock down the gateway
If you have any gateway adapter enabled, this is your largest remote attack surface. Three rules apply uniformly across all of them.
Allowlist. Every adapter must refuse to dispatch agent work, resolve approvals, or relay output until a caller allowlist is configured. The policy treats fail-open behavior as a bug - but the operator still has to set the list. For Telegram, that means the chat IDs (or user IDs) that are authorized to talk to your bot. For Slack, the workspace and user IDs. For email, the From addresses. Until you set this, anyone who guesses your bot token or scrapes a public Slack channel has the same access you do.
Session IDs are routing handles, not authorization. Knowing another caller's session ID grants no access. Authorization is re-checked against the allowlist on every call. You should not treat session URLs as secrets, and you should not embed authorization checks that rely on them.
Treat token leakage as account compromise. Telegram bot tokens, Slack tokens, Discord webhook URLs - if any of these leaks, an attacker with the token has whatever the bot can do. Rotate proactively, store in a secrets manager (not in the repo, not in your shell history), and audit access logs.
For network-exposed HTTP surfaces (the dashboard plugin, the API server, the kanban plugin), the defaults bind to loopback. Switching them to --host 0.0.0.0 is a break-glass operator decision. If you make it, you own the public-internet hardening - TLS, auth, rate limiting - none of which Hermes provides for you on that path.
Step 3 - Match the cloud environment to the posture
The cloud-deployment specifics that Hermes' own docs leave to the operator:
On AWS or any VPC-based cloud:
- Run Hermes in a private subnet. The agent does not need to be reachable from the public internet for any of the messaging gateways to work - those poll outbound or accept webhooks via a separate ingress that you control.
- Outbound security group: deny by default. Allowlist (a) your secrets manager endpoint, (b) the inference provider's endpoint (whichever LLM you've selected via
hermes model), (c) the messaging platforms in use, (d) your logging endpoint. Nothing else. - Inbound security group: deny by default. If a gateway uses inbound webhooks (Slack events API, Telegram webhook mode), put them behind an ALB with WAF and explicit path-based routing.
- IAM role attached to the EC2 / Fargate task: scoped to specific resources. The agent does not need
*:*on anything. If it does file work, scope to specific S3 buckets. If it queries databases, scope to specific Secrets Manager secrets and specific RDS hosts.
On Modal or Daytona (the serverless paths):
- The "hibernates when idle" feature is genuinely great for cost. It is also a different security model from a persistent VM: cold-start state and warm-start state may both be reachable. Keep credentials in the Modal/Daytona secret store, not in the image.
- Default network policy on Modal lets containers reach the internet broadly. If your workload doesn't need that, restrict it.
On a $5 VPS (the path most casual users will take):
- Disable password auth on SSH. Use keys. This is table stakes, but it is also the most common compromise vector for a small VPS.
- Run Hermes as a dedicated non-root user (the Docker image already does this - UID 10000). If you're running outside Docker, create the user explicitly.
- Put
ufw(or equivalent) in front of everything. Allow SSH from your IP only. Deny everything else inbound. - If you need the dashboard or API surfaces externally, front them with Tailscale or WireGuard rather than exposing them to the public internet. A free-tier Tailscale account covers a small operator deployment indefinitely.
Step 4 - Skills and plugins are code you're installing
Hermes' Skills system is one of the things that makes it powerful: the agent creates skills from experience, improves them during use, and can install community-contributed skills from external repositories. Plugins extend the architecture further - they load into the agent's Python process and run with full agent privileges.
This is - and the project says so explicitly - operator review surface. Skills Guard exists as a review aid that scans installable content for injection patterns. It is not a boundary. The supported workflow for third-party skills is:
- Read the actual Python and shell scripts in the skill, not just
SKILL.md. Skills execute arbitrary Python at import time. - Treat plugin installs the same way you treat installing any package from PyPI on a production system. Pin versions. Review the source. Treat the install audit log as evidence of what you've actually run.
- If you wouldn't install the underlying code on the system without review, don't install the wrapping skill or plugin either.
For credential handling, Hermes filters the environment passed to shell, MCP, and code-execution subprocesses (provider API keys and gateway tokens are stripped by default), but anything running inside the agent process - every skill, every plugin, every hook handler - can read whatever the agent itself can read. The mitigation is review-before-install. There is no in-process containment of plugin code, and the policy is explicit about that.
Step 5 - Add an in-process gate, knowing what it is
OS-level isolation is the boundary. In-process heuristics catch most of the day-to-day mistakes - the agent generating a rm -rf because of a confusing prompt, the agent reaching for git push --force because the LLM concluded that was the simplest path. These mistakes are common, they are usually not adversarial, and a sharper in-process gate prevents them from reaching the boundary at all.
Hermes' built-in approval gate does some of this. It detects common destructive shell patterns and asks the operator before execution. The policy is upfront that it's a denylist over shell strings and structurally incomplete - "the gate catches cooperative-mode mistakes, not adversarial output."
If you want the in-process gate to be sharper, you can layer one on. This is where Node9 fits in a Hermes deployment: an AST-based policy engine that parses a command's structure rather than pattern-matching its raw text. A naive denylist fires on the string rm -rf sitting inside a commit message - a false positive that trains operators to click through warnings - and misses a real destructive command split across a heredoc or command substitution. A structural engine resolves what the command actually does before deciding.
That structural view is what defeats obfuscation. Where a denylist gets fooled by \rm, quote-splitting, or a base64 pipe, parsing collapses the disguise back to the real command - and blocks it. The same engine runs a per-call inspection layer that flags credentials in outbound arguments, anomalously large payloads, and force-push patterns simple denylists miss. The approach is covered in detail in Why Regex Is Not Enough.
That inspection layer also answers the visibility question the OS boundary can't. Isolation contains a leak; it doesn't tell you that a credential sat in a tool argument on its way to an allowlisted endpoint, or that the agent spent an hour looping on the same file. node9 reads the arguments of every tool call and keeps an audit trail of what each call actually did - orthogonal to containment, not a substitute for it. Your egress security group will happily pass a secret it can't see inside.
node9 meets a Hermes deployment at two levels, and you pick by how much protection you want. Native mode - what npx node9-ai init gives you - drops into your existing agent and gates every tool call in-path, with the per-call inspection and audit trail above. It's the day-to-day layer: always on, no new infrastructure, live on Hermes today. Sandbox mode - node9 sandbox run - goes all the way to containment: a disposable box with a kernel-enforced egress allowlist and node9's own gate running inside it, for when you want the hard wall rather than an add-on. Same policy, same audit log, either way.
What node9 does not cover on Hermes, so you can plan around it: there is no prompt event, so a secret pasted into the prompt is not scanned until it shows up in a tool call; Hermes' MCP servers are not wrapped; cost is not tracked; and Hermes sends an empty session id on its pre-tool hook, so node9's output-taint review gate does not work for Hermes until the upstream fix lands (NousResearch/hermes-agent#48311). The tool-call gate and the audit trail are unaffected.
Sandbox mode runs Claude and Codex today with Hermes on the roadmap - so for Hermes right now, pair native mode with the whole-process wrapping from Step 1 and you get exactly that shape: a hard boundary around the agent, node9 governing and recording every call inside it. (When to choose native vs sandbox →)
Run node9 alongside OS isolation and the two cover different jobs: the OS contains the blast radius, node9 catches the dangerous command before it runs and records what every agent actually did. Isolation answers how much damage is possible; node9 answers what is the agent doing, and is it allowed - across every agent, in one place.
A 30-minute audit checklist
If you have Hermes running in the cloud right now, walk through this before you close your laptop.
Most of it is host state - what's listening, what's reachable, what secrets sit on disk, whether the gate actually blocks. node9 posture reads all of that and returns a 0-100 score with the highest-leverage fix first:
npx node9-ai posture
It covers five of the seven checks below (tagged posture). The other two are Hermes-application checks - they live in Hermes' own config and history, not on the host, so posture can't see them and you verify them by hand (tagged by hand).
- Isolation posture.
posture- Have you explicitly chosen one - terminal-backend or whole-process - and is the configuration consistent with the choice? "I'm not sure" is a finding. - Gateway allowlists.
by hand- Does every enabled adapter (Telegram, Slack, Discord, email, etc.) have an explicit allowlist? Send a message from a non-allowlisted account - does it actually refuse, or does it accept and let the LLM-level gate sort it out? - Inbound security group / firewall.
posture- From an external IP, what's reachable? The answer should be either nothing or only your ALB / webhook endpoint, never the agent process directly. - Outbound security group.
posture- From inside the agent container,curl https://example.com. It should fail. If it succeeds, you don't have egress control. - Secret storage.
posture- Are your gateway tokens, LLM provider keys, and SSH keys in a secrets manager / OpenShell Provider store, or in a.envfile on disk? - Skills installed.
by hand- List every skill installed. For each: did someone read the Python before running it? Or did it just get added via a one-line command? - Approval gate behavior.
posture- Trigger a destructive-looking command through a chat -rm -rf /tmp/testis safe to actually run. Does the gate intercept it? Note: many gates treat/tmpas scratch space and allow it by design, so also try a path the gate is meant to protect (your home dir, a credentials path) to see a real decision. Then try an obfuscated form (\rm …,echo … | base64 -d | sh). Still intercepted? Usually not all of them - and that's the point: no in-process gate is complete, which is exactly why OS isolation is the boundary.
One note on posture's output: when it flags isolation it points to node9 sandbox run, node9's containment mode - which covers Claude and Codex today, with Hermes on the roadmap. On a Hermes host you reach the same end state by pairing Step 1's whole-process wrapping with node9's native gate and egress lock; once the wrapping is in place, posture sees the container and the isolation finding goes green.
If any of these come back as "I'm not sure", that's the next thing to fix.
Closing
Hermes Agent is one of the more interesting things to land in the open-source agent space - model-agnostic, multi-platform, runs on infrastructure ranging from a Raspberry Pi to a GPU cluster. The flexibility is genuine. It also means the operator has more setup work than a typical desktop agent: gateway allowlists, isolation posture, terminal backend choice, skill review, credential scoping. None of it is exotic; all of it has to be done.
Two node9 commands map onto that work. npx node9-ai posture grades the host as it stands right now - isolation, egress, exposed ports, secrets on disk, the destructive-command gate - and returns a score with the first thing to fix. It's read-only and needs nothing wired, so it's a fair thing to run before you trust any of this. When you're ready to make the gate live, npx node9-ai init puts node9 in-path through the same shell-hooks system this guide describes, routing every tool call through an AST-based gate and inspection layer before it executes:
npx node9-ai init
It appends a hooks: block to ~/.hermes/config.yaml - the Step 5 in-process gate, live on your running agent, plus the per-call visibility and audit trail. Egress is a separate, opt-in step: node9 egress watch prompts on unknown hosts, node9 egress lock blocks them, and it gates destinations rather than contents. Local-only, no telemetry. Apache 2.0.
(If you also run Claude Code, Codex, Gemini, or Cursor, npx node9-ai scan reads their existing session logs and reports the risky tool calls already in your history. Hermes history scanning is on the roadmap - Hermes integrates live via hooks rather than writing session files, which is why the live init path is the one to use there. If you hit a snag, drop a note in the issues.)
If you've deployed Hermes Agent in the cloud and have war stories worth sharing - failure modes, hardening tricks, edge cases that surprised you - drop them in the comments. The boundary work is the part nobody writes about.