In Short
- Problem: I wanted to install Hermes locally without turning my Mac into open ground for an agent that can read and write everywhere.
- What I tested: a Codex session tasked with installing Hermes Agent, connecting it to Open WebUI, launching it as a macOS user service, and limiting filesystem access.
- Result: the local installation works with an API on
127.0.0.1:8642, Open WebUI on127.0.0.1:3000, a user LaunchAgent, a writable workspace, and repositories mounted read-only. - Lesson: most of the work was not “install an agent”. It was connecting Hermes, Codex
CLI, Docker, Open WebUI, and
launchdwithout losing the security boundaries. - Limit: I have not proven full reboot behavior, voice, load, multi-user usage, or any public network exposure.
Why I Wanted To Test This
Hermes interested me for a simple reason: I wanted to see whether a local agent could become a comfortable work interface, not just another command line.
On paper, Hermes Agent has several useful pieces: a CLI, an OpenAI-compatible API, memory, skills, integrations, and the option to connect a frontend such as Open WebUI. But my need was narrower than the official documentation.
I wanted a local Hermes setup, driven by Codex, usable from a web interface, with a clear filesystem boundary:
- one work folder where Hermes can write;
- my repositories visible in read-only mode;
- local secrets kept out of the article and any public repo;
- a service that restarts cleanly after
restart; - no port exposed beyond
localhost.
So the question was not: “does Hermes exist?” It was: can Codex assemble a usable local installation without making me lose control of the edges?
What Codex Actually Installed
The session worked inside a local tree sanitized here as
/Users/<LOCAL_USER>/repos/Hermes.
The core setup looked like this:
Open WebUI container
-> host.docker.internal:8642/v1
-> Hermes Gateway API
-> Codex CLI app-server
-> backend Codex/ChatGPT
The verified local version was Hermes Agent v0.15.1 (2026.5.29), with
codex-cli 0.130.0. Open WebUI ran in Docker from the
ghcr.io/open-webui/open-webui:main image.
The interesting part was not one magic command. Codex had to build a small local installation around Hermes:
/Users/<LOCAL_USER>/repos/Hermes
├── AGENTS.md
├── README.local.md
├── allowed-folders.yaml
├── app/
│ ├── agent/transports/codex_app_server.py
│ └── gateway/platforms/api_server.py
├── bin/
│ ├── hermes-local
│ ├── hermes-service
│ ├── run-hermes-service
│ ├── start-hermes-gateway
│ └── start-open-webui
├── home/
│ ├── .env
│ ├── auth.json
│ ├── config.yaml
│ ├── logs/
│ ├── sessions/
│ └── *.db
├── open-webui/
└── workspace/
app/ contains the Hermes code. bin/ contains the local wrappers. home/ contains
state, logs, sessions, SQLite databases, and secrets. workspace/ is the only work area
intended for writes.
I liked that split because it forces a simple separation: what runs Hermes, what stores sensitive state, and what the agent can modify.
The Happy Path
At the end, local control looked like this:
bin/hermes-service install
bin/hermes-service status
bin/hermes-service restart
bin/hermes-service logs
bin/hermes-service uninstall
The macOS service is a user LaunchAgent:
com.pezzos.hermes.local
It starts a local supervisor, starts Hermes Gateway, waits for the API, then starts or
recreates the Open WebUI container. The container points to Hermes through
host.docker.internal, so the API does not need to listen on a wider network address.
The minimal checks are direct:
curl http://127.0.0.1:8642/health
curl http://127.0.0.1:3000/
docker ps --filter name=hermes-open-webui
And the test that mattered after the fix was streaming:
curl -N \
-H "Authorization: Bearer <API_SERVER_KEY>" \
-H "Content-Type: application/json" \
http://127.0.0.1:8642/v1/chat/completions \
-d '{"model":"gpt-5.5","stream":true,"messages":[{"role":"user","content":"Answer exactly: OK"}]}'
The final trace showed an SSE response with delta.content, followed by the validation
message: SERVICE RESTART OK.
The First Useful Error: An Empty Answer In Open WebUI
The most telling bug was not spectacular.
Open WebUI showed titles and follow-ups, but not the assistant response content. That is
the kind of failure that makes it tempting to blame the interface, the model, or the
frontend. Codex checked lower down instead, with a curl -N request to
/v1/chat/completions.
The diagnosis was more precise: SSE chunks were arriving, but without delta.content.
Open WebUI received a stream, but not the text fragment it expected in order to display
the answer.
The fix was made on the Hermes side, in app/gateway/platforms/api_server.py: when no
text delta had been emitted during streaming, the API backfilled the final_response.
That is not a grand abstract lesson. It is just a useful field trace: connecting an “OpenAI-compatible” API to an interface does not mean every streaming detail is right on the first try.
The Second Useful Error: launchd Could Not Find Codex
The second problem came from macOS.
When launched by hand, Codex was available. When launched from the service, Hermes hit:
FileNotFoundError: [Errno 2] No such file or directory: 'codex'
This was not really a Hermes problem. It was an environment problem: a launchd service
does not inherit the same PATH as an interactive shell. Homebrew can be visible in my
terminal and invisible to the LaunchAgent.
The fix was to stop assuming that codex would be findable everywhere. The code now
resolves the binary in this order:
HERMES_CODEX_BIN;codexonPATH;/opt/homebrew/bin/codex;/usr/local/bin/codex.
This detail is almost more useful than the installation itself. As soon as a local agent becomes a service, the small assumptions of the shell become local production bugs.
The Boundary I Cared About
I did not want to give Hermes write access to all my repositories.
The final configuration keeps a simple rule:
/workspace -> workspace/ # write
/output -> home/cache/documents/ # write
/mnt/repos -> /Users/<LOCAL_USER>/repos # read-only
In plain language: Hermes can work in its workspace, produce documents in a dedicated output area, and read my repositories. To write elsewhere, the configuration would have to change explicitly.
That is what I want to keep from this experiment. The comfort of a local agent should not depend on broad default access. A good setup says where the agent can write before the agent starts writing.
Security, Without Pretending
The setup is local. The two important ports listen on localhost:
127.0.0.1:8642for the Hermes API;127.0.0.1:3000for Open WebUI.
There is no TLS, no reverse proxy, and no public exposure. That is acceptable for what I tested precisely because I did not open these ports to the outside.
Secrets stay local:
home/auth.jsoncontains Codex auth, with600permissions;home/.envcontains possible sensitive variables, with600permissions;home/config.yamlcontainsAPI_SERVER_KEYin clear text;- Open WebUI receives that key through a Docker environment variable.
There are useful guardrails: home/ in 700, sensitive files in 600, repositories
mounted read-only, and a limited writable workspace. But that does not turn the
installation into a hardened system.
I would not copy four things blindly:
- reusing a personal Codex auth file on a shared machine;
- leaving an API key in clear text if the context goes beyond a personal local setup;
- exposing
3000or8642on the network; - adding broad writable volumes just to save time.
The setup works because it stays local. If I wanted to make it reachable from another device, I would need to rethink auth, networking, logs, and permissions. I would not treat that as a simple Docker option to add.
What Is Still Missing
There are several things I have not proven.
I have not done a full Mac reboot test. The service restarts after restart, but I have
not yet validated the whole login, Docker Desktop, LaunchAgent, API, and WebUI sequence.
I have not tested voice. The Hermes documentation mentions TTS features, but this run does not prove end-to-end voice usage.
I have not tested load, long sessions, concurrent calls from Open WebUI, or real multi-user usage.
I also did not pin Open WebUI to a stable version. The image used was
ghcr.io/open-webui/open-webui:main. For a reproducible setup, I would prefer an
explicit version or digest, not a moving tag.
Finally, I have not audited logs, SQLite databases, sessions, and plists as if they were going to be published. For an article, the sanitized trace is enough. For a public repo, that would be a different level of cleanup.
What I Keep
I thought the article would mainly describe an automated installation. In practice, what I want to keep is the list of interfaces that had to become explicit.
Hermes had to speak to Open WebUI in the right format. Open WebUI had to reach the API
from Docker. launchd had to find Codex without inheriting my shell. The workspace had
to be writable, but the repositories did not. Secrets had to stay local. Logs could not
accidentally become public sources.
That is where Codex was useful. Not because it launched one install command for me, but because it could follow errors one by one, verify endpoints, change wrappers, fix streaming, and make the service restartable.
The result is partial, but concrete: a local Hermes installation usable from Open WebUI, launched as a user service, with visible limits.
What You Can Reuse
If you want to repeat something similar, I would mostly keep the method:
- install locally first, without network exposure;
- check
/health,/v1/models, and/v1/chat/completionsbefore judging the UI; - test streaming with
curl -N, not only in the browser; - launch from
launchdearly, to revealPATHdifferences; - separate
home/,workspace/, read-only repositories, and logs; - write
uninstallbefore considering the setup finished.
What I would not reuse without thinking: the Open WebUI main image, the clear API key
in personal config, or the idea of exposing the service outside localhost.
For me, Hermes becomes interesting when it remains inspectable. Not when it can do everything, but when I can say precisely where it runs, what it can read, what it can write, how it fails, and how I stop it.
The Companion Repo
The real complement to this article should not be my local folder. It contains Codex auth, config with an API key, logs, SQLite databases, and personal paths. That is not safe to share, and it would not be healthy to ask someone else to copy it.
The useful artifact is a small macOS companion repo, still local for now, with two uses:
keep the Codex prompt that installs Hermes with the guardrails, and provide
scripts/templates to replay the setup without reinventing launchd, Open WebUI,
checks, and uninstall.
Its current shape looks like this:
prompts/codex-install-hermes-local.md
prompts/codex-verify-hermes-local.md
scripts/bootstrap-hermes-local.sh
scripts/check-hermes-local.sh
scripts/hermes-service.sh
templates/allowed-folders.yaml
templates/com.pezzos.hermes.local.plist.template
templates/hermes.env.example
RESULTATS.md
The local repo already passes static checks and dry-runs. It is not published yet. While it remains local, this article stays an experience note, not a reproducible base to copy.