This article is published in English.
Practical notes: Give Hermes Agent Secure Web Access with a Separate Chrome
Operable walkthrough of Practical notes: Give Hermes Agent Secure Web Access with a Separate Chrome: contracts, checks, and drop-in code slots for teams shipping this pattern.
The following notes reconstruct a practical path around “Give Hermes Agent Secure Web Access with a Separate Chrome Podman Pod”. Emphasis stays on contracts, checks, and drop-in code placeholders rather than motivational framing. When working through the Overview stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
INTERNET
▲
│
outbound only
│
┌────────────────────────┐
│ Chrome Browser Pod │
│ │
│ Chrome Headless │
│ CDP :9222 │
└───────────┬────────────┘
│
private CDP network
│
┌───────────▼────────────┐
│ Hermes / Ollama Pod │
│ │
│ Hermes ─────► Ollama │
│ localhost:11434
│ │
└────────────────────────┘
│
▼
AMD GPU
Chrome → Internet YES
Hermes → Chrome YES
Hermes → Ollama YES
Hermes → Internet directly NO
Chrome → host filesystem NO
Hermes → rest of host NO
Either → Podman socket NO
Why put Chrome in a separate pod?
The Why put Chrome in stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
Untrusted Internet content
↓
Chrome container
↓
CDP
↓
Hermes
Why CDP?
The Why CDP stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
chromedp/headless-shell
The network design
The The network design stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts. The The network design stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
hermes-browser-link
browser-egress
Internet
▲
│
browser-egress
│
Chrome Pod
│
hermes-browser-link
│
Hermes Pod
hermes-browser-link
Step 1 — Stop the existing Hermes environment
For the Step 1 Stop the stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.
systemctl --user stop hermes-local
systemctl --user disable hermes-local
podman volume ls
hermes-data
ollama-models
Step 2 — Choose a private subnet
For the Step 2 Choose a stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.
10.77.0.0/24
Hermes pod: 10.77.0.10
Chrome pod: 10.77.0.20
podman network ls
podman network inspect NETWORK_NAME
Step 3 — Create the private browser-control network
For the Step 3 Create the stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness. For the Step 3 Create the stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
podman network create \
--internal \
--subnet 10.77.0.0/24 \
hermes-browser-link
podman network inspect hermes-browser-link
Step 4 — Create the browser egress network
When working through the Step 4 Create the stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
podman network create browser-egress
Step 5 — Recreate the Hermes pod
When working through the Step 5 Recreate the stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
podman pod rm -f hermes-local
podman pod create \
--name hermes-local \
--network hermes-browser-link:ip=10.77.0.10 \
--userns=keep-id:uid=10000,gid=10000
10.77.0.10
Step 6 — Recreate Ollama
When working through the Step 6 Recreate Ollama stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Log request id, model id, and latency on every call. Without that trail, intermittent provider errors look like application bugs. When working through the Step 6 Recreate Ollama stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
podman run -d \
--name ollama \
--pod hermes-local \
--device /dev/kfd \
--device /dev/dri \
--group-add keep-groups \
-e HOME=/root \
-e OLLAMA_MODELS=/root/.ollama/models \
-e OLLAMA_CONTEXT_LENGTH=64000 \
-v ollama-models:/root/.ollama \
docker.io/ollama/ollama:rocm
http://127.0.0.1:11434
podman exec ollama ollama list
Step 7 — Recreate Hermes
The Step 7 Recreate Hermes stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
export HERMES_WORKSPACE="$HOME/path/to/Hermes-Workspace"
podman run -d \
--name hermes \
--pod hermes-local \
--security-opt=no-new-privileges \
--pids-limit 512 \
-v hermes-data:/opt/data \
-v "$HERMES_WORKSPACE:/opt/data/workspace:rw,nodev,nosuid" \
-w /opt/data/workspace \
docker.io/nousresearch/hermes-agent:latest \
sleep infinity
RW access:
$HERMES_WORKSPACE
/
$HOME
~/.ssh
~/.config
your normal browser profile
Podman socket
Docker socket
Step 8 — Pull Chrome Headless Shell
The Step 8 Pull Chrome stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
podman pull docker.io/chromedp/headless-shell:latest
Step 9 — Create a separate Chrome pod
The Step 9 Create a stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts. The Step 9 Create a stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
podman pod create \
--name hermes-browser-pod \
--network hermes-browser-link:ip=10.77.0.20 \
--network browser-egress
Chrome
│
├── 10.77.0.20 ─── private link to Hermes
│
└── browser-egress ─── Internet
Step 10 — Run Chrome inside the browser pod
For the Step 10 Run Chrome stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.
podman run -d \
--name hermes-browser \
--pod hermes-browser-pod \
--init \
--shm-size=2g \
--security-opt=no-new-privileges \
docker.io/chromedp/headless-shell:latest
-p 9222:9222
10.77.0.20:9222
Step 11 — Test Chrome from the Hermes container
For the Step 11 Test Chrome stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.
podman exec hermes python -c \
'import urllib.request; print(urllib.request.urlopen("http://10.77.0.20:9222/json/version").read().decode())'
{
"Browser": "HeadlessChrome/...",
"webSocketDebuggerUrl": "ws://10.77.0.20:9222/devtools/browser/..."
}
Hermes → Chrome
Step 12 — Verify that Chrome has Internet access
For the Step 12 Verify that stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness. For the Step 12 Verify that stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
podman exec hermes-browser \
curl -I https://example.com
browser-egress
Step 13 — Verify Hermes does NOT have direct Internet access
When working through the Step 13 Verify Hermes stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
podman exec hermes python -c \
'import urllib.request; print(urllib.request.urlopen("https://example.com", timeout=5).status)'
podman exec hermes python -c \
'import urllib.request; print(urllib.request.urlopen("http://10.77.0.20:9222/json/version").status)'
200
Hermes → Internet FAIL
Hermes → Chrome PASS
Chrome → Internet PASS
Step 14 — Point Hermes at the remote Chrome instance
When working through the Step 14 Point Hermes stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
http://127.0.0.1:9222
http://10.77.0.20:9222
podman exec \
--user 10000:10000 \
hermes \
hermes config set browser.cdp_url http://10.77.0.20:9222
podman exec hermes \
grep -A3 '^browser:' /opt/data/config.yaml
browser:
cdp_url: http://10.77.0.20:9222
podman restart hermes
Step 15 — Test it from Hermes
When working through the Step 15 Test it stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node. When working through the Step 15 Test it stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
podman exec -it \
--user 10000:10000 \
-w /opt/data/workspace \
hermes \
hermes
Important: this is browser access, not unrestricted Hermes Internet access
The Important this is browser stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
Hermes browser tools
↓
Chrome
↓
Internet
curl https://example.com
Why not put Chrome in the Hermes pod?
The Why not put Chrome stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
127.0.0.1:9222
Hermes pod:
private network only
Browser pod:
private network
+
Internet network
Do not mount your normal Chrome profile
The Do not mount your stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts. The Do not mount your stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
-v "$HOME/.config/google-chrome:/chrome-profile"
Optional: persistent agent browser profile
For the Optional persistent agent browser stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.
podman volume create hermes-browser-profile
Human browser profile separate
Agent browser profile separateHost filesystem separate
CDP should stay private
For the CDP should stay private stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.
10.77.0.20:9222
hermes-browser-link
0.0.0.0:9222 -> Chrome
Verify the isolation
For the Verify the isolation stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness. For the Verify the isolation stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
podman pod inspect hermes-local
podman pod inspect hermes-browser-pod
Hermes pod
└── hermes-browser-link
Browser pod
├── hermes-browser-link
└── browser-egress
podman inspect hermes \
--format '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}'
podman inspect hermes-browser \
--format '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}'
podman port hermes-browser
Starting both pods automatically
When working through the Starting both pods automatically stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
nano ~/.config/systemd/user/hermes-local.service
[Unit]
Description=Hermes Local AI and Browser Pods
After=default.target
[Service]
Type=oneshot
RemainAfterExit=yesExecStart=/usr/bin/podman pod start hermes-browser-pod
ExecStart=/usr/bin/podman pod start hermes-localExecStop=/usr/bin/podman pod stop -t 30 hermes-local
ExecStop=/usr/bin/podman pod stop -t 30 hermes-browser-podTimeoutStartSec=120
TimeoutStopSec=60[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now hermes-local
sudo loginctl enable-linger "$USER"
podman pod ps
hermes-local
hermes-browser-pod
Troubleshooting
When working through the Troubleshooting stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
Hermes can’t reach port 9222
When working through the Hermes can t reach stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node. When working through the Hermes can t reach stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
podman ps
podman exec hermes python -c \
'import urllib.request; print(urllib.request.urlopen("http://10.77.0.20:9222/json/version").read().decode())'
hermes-browser-link
Chrome can start but pages crash
The Chrome can start but stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
--shm-size=2g
Chrome works but Hermes browser tools don’t appear
The Chrome works but Hermes stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve.
browser:
cdp_url: http://10.77.0.20:9222
nano isn't available inside the Hermes container
The nano isn t available stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts. The nano isn t available stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish.
hermes config set
podman exec \
--user 10000:10000 \
hermes \
hermes config set browser.cdp_url http://10.77.0.20:9222
The finished security model
INTERNET
▲
│
browser-egress
│
┌─────────┴──────────┐
│ Chrome Browser Pod │
│ │
│ Headless Chrome │
│ CDP :9222 │
└─────────┬──────────┘
│
hermes-browser-link
INTERNAL ONLY
│
┌─────────▼──────────┐
│ Hermes/Ollama Pod │
│ │
Host Workspace ─►│ Hermes │
RW │ │ │
│ ▼ │
│ Ollama │
│ │ │
└─────┼──────────────┘
▼
AMD GPU
ACCESS
Hermes → designated host workspace YES
Hermes → Ollama YES
Hermes → Chrome CDP YESChrome → Internet YESHermes → Internet directly NO
Hermes → rest of host filesystem NO
Hermes → SSH keys NO
Chrome → host filesystem NO
Chrome → normal browser profile NO
Hermes → Podman/Docker socket NO
Chrome → Podman/Docker socket NO
Host/LAN → Chrome CDP NO
Why you prefer this architecture
Give it the filesystem.
Give it Docker.
Give it Internet.
Give it Chrome.
Give it credentials.
one working directory
local-model access
browser-control access
model storage
GPU access
Internet access
CDP connectivity
my SSH keys
my entire home directory
my normal browser profile
my Podman socket