TLDR: I moved from a local OpenClaw setup in a tiny local macOS VM to a self-hosted Hermes Agent setup on a Hetzner VPS. I still talk to it through Telegram, but it now keeps running when my laptop is closed, has a safer file bridge to my Mac, can use Claude Code both inside and outside the Daocker container, and browses using a residential IP. This is written from my current setup: ‘local machine’ means my MacBook, and Anthropic and OpenAI Pro accounts are both used. The VPS runs on Ubuntu 24.04 LTS.

Why move away from the local VM setup?

With agent harnesses, many of us are trying the same thing: a personal assistant that knows how you work, your preferences & goals, etc. Not a chatbot that forgets everything each session. My first setup used OpenClaw in a tiny dedicated macOS VM via Lume. It was isolated from my host system, talked to me through Telegram, had its own Google Workspace account, and was version-controlled via GitHub & VM snapshots.

I’ve been very happy with my previous OpenClaw setup and had managed to get consistent performance out of it - given I wouldn’t update too often. Though I heard that part has got a lot better already. One Claude Max subscription gave me access to the best agent-capable model available at the time (in my opinion), and the whole thing felt surprisingly assistant-like. Then Anthropic tightened the boundary around Claude subscriptions and third-party harnesses. OAuth authentication for Claude subscriptions is now intended for Claude Code / Claude.ai and native Anthropic applications, not arbitrary third-party agent harnesses anymore.

So the next setup had two goals:

  1. Move the agent to something always-on, so it does not die when I close the laptop.
  2. Keep the “Opus feel” as much as possible while moving to Hermes and GPT-5.5 (since subscription use is allowed at the moment).

The setup that works for me

The new setup runs on a VPS with Ubuntu 24.04 LTS, setting me back ~15$ each month. Hermes and its monitoring dashboard run in Docker. I like the clear divide between any secrets, my customizations and the easy-to-update Hermes container. In general, updates don’t break things anymore so far. The costs are keeping oversight of (and occasionally resetting) users & permissions on the VPS, to keep infra security according to the plan. A Claude Code instance outside the Hermes docker helps with installs & maintenance. The full deployment plan also sits there to prevent drift from the planned security architecture, next to an actively maintained live progress doc. Components:

High-level Hermes VPS setup architecture

The VPS can only be reached with my SSH key, root user is deactivated. Agent traffic from inside the container reaches the internet directly (APIs etc.). For browser-based or ‘human-like’ tasks, Hermes’ Camofox browser plugin is set up to look inconspicuous and can only access the web via my actual home router (WireGuard tunnel + microsocks), keeping a low profile. File I/O works for me by dumping into or reading from the ‘shared folder’, and Hermes knows what I mean when referencing it. Normal interaction with Hermes via Telegram, maintenance via SSH.

Updating the Hermes container is trivial with docker compose build --pull && docker compose up -d and doesn’t touch any secrets or config outside the container.

Getting back the Opus feel with GPT-5.5

I was and still am shocked of GPT5.5’s default verbosity. Who wants to read all of that filler? What a waste of time. And the sycophancy, terrible. I found myself scrolling endlessly through single messages on Telegram. Things needed to change, drastically. I had host Claude look at the current config & recent messages, do research and described in detail how I’d like the agent’s behavior to change. Some examples:

LeverWhat changed
SOUL.mdAdded a short “pragmatic senior engineer” baseline plus explicit style, disagreement, and anti-sycophancy rules.
USER.mdAdded stable preferences: concise Telegram output, debug-first work, account-safe automation, repo conventions.
agent.personalitiesAdded overlays like concise, brainstorm, review, and research (I keep concise active 99% of the time).
agent.reasoning_effortSet a reasoning mode for the main agent instead of letting the model decide.
agent.tool_use_enforcementSet to/kept on auto. Apparently the setting nudges to actually use tools.

config.yaml snippet:

agent:
  service_tier: priority
  reasoning_effort: medium
  tool_use_enforcement: auto
  personalities:
    concise: >
      Single-paragraph or single-table when possible. No preamble, no summary.
      If a one-line answer fits, give one line. Skip tool preambles for tasks
      under 3 tool calls.
    review: >
      Rigorous reviewer. For each block sent: (1) what's correct, (2) what's
      wrong with a concrete reason, (3) minimal fix. No praise, no restatement.
      Disagree when there's evidence.

SOUL.md snippet:

# Avoid
- Sycophancy: "Great question", "Happy to help", "Got it", "Sure",
  "Of course", "Certainly".
- Preambles. Don't rephrase the request before answering.
- Tool preambles for tasks under 3 tool calls ("I'll start by reading...",
  "Let me first check...").
- Trailing summaries when the answer is already short. No recap of what
  the user can already read.
- Hype language: "incredible", "powerful", "seamlessly".

The final voice is much closer to what I want. Short by default, willing to push back, no fake enthusiasm, fewer disclaimers around tool use.

Clean, easy file exchange with the local Mac

I wanted file I/O between my laptop and the VPS to feel easy without giving the agent broad access to my local machine.

The tiny tool Syncthing is doing that really well, creating a Dropbox-like folder syncing instantly both ways: If I place a file into the shared folder on the Mac, it appears on the VPS. If the Hermes agent creates anything and dumps it in there, it appears on the Mac. Works flawlessly now, after some initial permission headache (Syncthing needs to ignore incoming permission bits for this folder from the Mac + I added ACL permission defaults & tiny watcher service on the VPS side).

Minimal diagram of Syncthing and SSH access paths

There are also three SSH entry points I use from the Mac:

ProfileLands whereUse
VPS shellhost shellsystem checks, Docker, logs
Docker shellHermes container via dshinspect /opt/data, config, shared folder
Host Claude tmuxpersistent tmux with host Claude Codelonger infra/debugging work that should survive laptop sleep

Browsing like a human (or at least failing visibly)

Those who tried to have an agent do things on their behalf know that sometimes they need to click buttons in a logged-in session and behave like a human. And this browser ability was the thing I was most worried about losing when moving from a macOS VM to a Linux VPS. In the old setup, I could literally watch the browser. If the agent failed, I could see whether it clicked the wrong button, got blocked by Cloudflare, lost a login session, and so on.

I could recreate that visibility through the Hermes plugin Camofox and noVNC. To add stealth and avoid a data center IP, I repurposed a 2009 Mac Mini that I had lying around and turned it into a permanent WireGuard tunnel endpoint (flash-booted Ubuntu Server, a Pi would do the same job). So:

Browser fingerprint: Firefox on Linux, language & timezone configurable, residential IP. For it to beat the Cloudflare turnstile, it took some training using its browser tools (and preventing quicker shortcuts it really liked). Some lessons the agent updated its skill with:

- Use `browser_type`, `browser_click`, and `browser_press` for interaction. Don't write form values through JS.
- Resnapshot after any meaningful page change. Don't reuse old ref IDs.
- React typeaheads need real input events and sometimes ArrowDown / Enter.
- Cross-origin cookie banners are a real limit: if the iframe is not in the accessibility tree, there is nothing to click.
- Cloudflare Turnstile checkboxes are sometimes solvable with Tab + Space, especially when verifying focus visually first.

I can now see what the agent does in a browser again. When the browser is used, I can open a browser tab to https://<vps-ip>:<port>/vnc.html, enter a simple token and watch the browser in real time, as stream running over WebSocket inside the VPS. Browser traffic is strictly routed through the WireGuard tunnel to the Mac Mini’s home ISP connection before it reaches the web. The outside world sees my home router’s residential IP and a (hopefully) perfectly ordinary Firefox-on-Linux browser fingerprint.

Maintenance & Docker path pain

With this setup I have

But for me, one big downside of having Hermes run in a container has been the constant docker compose -f <path> exec ... prefix pain for half the cmds you use (the hermes CLI lives inside the container). So over time, I added more and more wrapper aliases that I use & have encouraged Claude Code to use, making his actions more readable & easier to approve/whitelist:

AliasUseReplaces
hermes <args>Run the Hermes CLI inside the containerdocker compose -f /home/hermes/hermes-stack/docker-compose.yaml exec hermes /opt/hermes/.venv/bin/hermes
dshInteractive shell in the Hermes containerdocker compose -f /home/hermes/hermes-stack/docker-compose.yaml exec --workdir /opt/data hermes bash
ddo <args>Run one command in the Hermes containerdocker compose -f /home/hermes/hermes-stack/docker-compose.yaml exec --workdir /opt/data hermes
hchat -q "ping"Manually trigger Hermes agent (so Claude can test Hermes)docker compose -f /home/hermes/hermes-stack/docker-compose.yaml exec hermes /opt/hermes/.venv/bin/hermes chat -q "ping"
dlogFollow Docker compose logs with readable timestampsdocker compose -f /home/hermes/hermes-stack/docker-compose.yaml logs -f -t plus timestamp cleanup with sed
alogFollow the Hermes agent logsudo tail -f /home/hermes/.hermes/logs/agent.log plus timestamp cleanup with sed
cronlogShow the latest cron outputddo bash -c 'find /opt/data/cron/output -name "*.md"; sort newest; head -1; cat file'
hermerrShow recent warnings/errorssudo tail -80 /home/hermes/.hermes/logs/errors.log
hermgwShow recent gateway/platform eventssudo tail -80 /home/hermes/.hermes/logs/gateway.log

These commands/definitions now live in a canonical /usr/local/lib/hermes-cmds.sh, with thin wrappers only in /usr/local/bin. That change made them work from a normal shell, from cron, and over non-interactive SSH. Old-school shell aliases would not. The agent and I can both refer to the same paths, aliases, logs, and state files.

AND - they now work when my local laptop’s Claude Code SSH’es into the VPS too, making command approvals more readable & easier to whitelist.

Some recommendations to prevent frustration

It feels good. Finally my agent

And on top of that, I can now hand off tasks from my local Claude Code to the VPS (e.g. when I have a bad connection - tmux is a real life saver here), or even use Termius and log into VPS Claude Code staight from the phone. So yeah, my Claudes can talk to each other - kind of. :)

Things that help me not get lost: