Home / Articles / Practical notes: Give Hermes Agent Secure Web Access with a Separate Chrome

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.

4114 words

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

Operational checklist