You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In the official nousresearch/hermes-agent:latest Docker image (verified on v0.12.0 (2026.4.30)), the dashboard's embedded Chat tab (hermes dashboard --tui) is unusable out of the box due to four compounding bugs. Each one alone is enough to break the chat; fixing them in isolation produces a different failure mode each time, which made this take hours to diagnose.
TL;DR of the four bugs, in the order they surface as you fix them:
/opt/hermes/ui-tui/ is root-owned → TUI rebuild fails with EACCES → banner reads "Chat unavailable: 1"
Files inside /opt/data/ (notably auth.json mode 0600) are root-owned → auth store unreadable → Models/Providers picker is empty and WARNING hermes_cli.auth: failed to parse /opt/data/auth.json [Errno 13] spams the logs
gosu preserves HOME=/root from Docker's default root start → TUI subprocess (node ui-tui/dist/entry.js) inherits HOME=/root, can't write to ~/.hermes, and silently produces no output — the Chat tab renders as a blank dark canvas with just a cursor blinking in the top-left corner
_hermes_ink_bundle_stale looks for ink-bundle.js but the build produces entry-exports.js → bundle is always considered stale → every Chat tab connection triggers a synchronous npm run build && tsc && npm run build:compile chain on the asyncio event loop, blocking the dashboard for minutes per connection, while the rebuild never produces the file the check is looking for, so the next connection does the same thing again. WebSocket handshake to /api/pty times out before accept() is reached.
After all four are worked around, chat works correctly (WebSocket connects in < 1s, slash workers spawn, OpenRouter/DeepSeek calls succeed, ink TUI renders).
The dashboard runs as the unprivileged hermes user (after the entrypoint drops privileges via gosu), but /opt/hermes/ui-tui/ and its dist/ directories are baked into the image as root:root. When _make_tui_argv() tries to rebuild the TUI bundle on first run, esbuild fails to write to /opt/hermes/ui-tui/packages/hermes-ink/dist/entry-exports.js with EACCES, the build aborts, _make_tui_argv calls sys.exit(1), and web_server.pty_ws reports Chat unavailable: {SystemExit(1)} over the WebSocket.
Bug #2 — /opt/data files are root-owned (entrypoint chown is non-recursive)
The entrypoint's needs_chown check only fires when the top-level$HERMES_HOME directory has the wrong owner — root-owned files inside/opt/data are left untouched. We hit this on /opt/data/auth.json (mode 0600), which made the auth store unreadable for the hermes user:
WARNING hermes_cli.auth: auth: failed to parse /opt/data/auth.json
([Errno 13] Permission denied: '/opt/data/auth.json') —
starting with empty store. Corrupt file preserved at /opt/data/auth.json.corrupt
This warning repeats roughly once per second forever. Symptom in the dashboard: the Models / Providers picker is empty because the auth store has no readable entries (even though OPENROUTER_API_KEY is set in the environment and the credential is reachable via env-pool fallback for direct CLI calls).
This was the killer. After fixing #1 and #2, the Chat tab still rendered blank — a dark canvas with just a cursor in the top-left, no spinner, no banner, no error.
Root cause: Docker starts the container as root by default, with HOME=/root from the env. When the entrypoint runs exec gosu hermes "$0" "$@", gosu by default preserves the parent's environment, including HOME. So the gateway and dashboard processes run as uid=hermes but with HOME=/root — a directory the hermes user has no read or write access to.
The TUI subprocess (node /opt/hermes/ui-tui/dist/entry.js) writes to ~/.hermes/ for skin caches, history, ink-cli state, etc. Every write fails silently, ink renders nothing, the WebSocket pumps an empty PTY stream, and the Chat tab shows a blank xterm.
Verified by inspecting /proc/<dashboard-pid>/environ:
HOME=/root
HERMES_HOME=/opt/data
Workaround: set HOME=/opt/data (the hermes user's actual passwd-listed homedir) in the container env. Then the chat works end-to-end.
Bug #4 — _hermes_ink_bundle_stale looks for a file that never exists, triggering a synchronous rebuild on every chat connection
This was the killer of killers. After working around bugs #1–#3, the Chat tab still hangs: the WebSocket to /api/pty opens a TCP connection, sends the upgrade request, and the server returns zero bytes — for minutes — until the browser times out.
Root cause: _make_tui_argv calls _tui_build_needed → _hermes_ink_bundle_stale, which checks for /opt/hermes/ui-tui/packages/hermes-ink/dist/ink-bundle.js. That file does not ship in the image (only entry-exports.js does, after bug #1 is worked around). So _hermes_ink_bundle_stale always returns True → _tui_build_needed always returns True → _make_tui_argv always runs subprocess.run([npm, "run", "build"], capture_output=True) synchronously.
That subprocess.run blocks the asyncio event loop for the duration of the build (npm + tsc + esbuild bundling — multiple minutes). During that time, every HTTP request and WebSocket upgrade on the dashboard hangs. The browser hits its 10-second WebSocket open timeout long before the build finishes.
To make matters worse: when the build does eventually complete, it still doesn't produce ink-bundle.js (the build script targets entry-exports.js). So the next connection starts the same rebuild from scratch.
Verified by capturing the dashboard's child process tree mid-hang:
hermes 40 /opt/hermes/.venv/bin/hermes dashboard --host 0.0.0.0 --tui
hermes 955 sh -c npm run build --prefix packages/hermes-ink && tsc -p tsconfig.build.json && npm run build:compile && chmod +x dist/entry.js
And by checking the file the staleness check is looking for:
Workaround: setting HERMES_TUI_DIR=/opt/hermes/ui-tui in the container env makes _make_tui_argv take its prebuilt-bundle shortcut (which checks dist/entry.js, not ink-bundle.js) and skip the rebuild path entirely. The chat then connects in < 1s.
Reproduction
Pull and run the official image with the dashboard in --tui mode:
TUI build failed.
> hermes-tui@0.0.1 build
> npm run build --prefix packages/hermes-ink && tsc -p tsconfig.build.json && ...
✘ [ERROR] Failed to write to output file:
open /opt/hermes/ui-tui/packages/hermes-ink/dist/entry-exports.js: permission denied
ls -la /opt/hermes/ui-tui/dist /opt/hermes/ui-tui/packages/hermes-ink/dist shows both directories owned by root:root in the shipped image, while the running process is uid=hermes.
Combined workaround (compose-level)
All three bugs can be papered over without touching the image, by:
Mounting an entrypoint wrapper that chowns both paths as root before chaining to the upstream entrypoint
After all three workarounds are in place, the chat works end-to-end. Without all three, you get one of the three failure modes above (banner, empty picker, or blank canvas).
Suggested upstream fixes
Any of these would resolve the bugs they correspond to. Ideally all three areas get patched:
Dockerfile: RUN chown -R hermes:hermes /opt/hermes/ui-tui after the build stage that produces dist/.
Or: tighten _tui_need_npm_install / _tui_build_needed so a fresh image with a complete prebuilt dist/ doesn't trigger a rebuild attempt at all. The entry.js is already present and runnable; the rebuild is only needed in dev mode.
Entrypoint: change the needs_chown check from a top-level stat to a recursive ownership check, OR just always run chown -R hermes:hermes "$HERMES_HOME" (cheap on a small data dir).
Or: tighten the Dockerfile so anything baked into /opt/data ships with the right ownership.
Pick one of these — there are several reasonable fixes:
Make the package's build target produce ink-bundle.js (or whatever name _hermes_ink_bundle_stale looks for). The mismatch between the build output (entry-exports.js) and the staleness check looks like a rename that wasn't propagated.
Or: change _hermes_ink_bundle_stale to look at the file the build actually produces.
Or: set ENV HERMES_TUI_DIR=/opt/hermes/ui-tui in the Dockerfile so the prebuilt-bundle shortcut is used by default in the official image.
Independent of the above, the rebuild path should not block the asyncio event loop. _resolve_chat_argv is called from inside pty_ws before await ws.accept(). If a build is genuinely needed, it should run via asyncio.to_thread / loop.run_in_executor, with a "Building TUI bundle…" message streamed to the client. Today, every connection that triggers it freezes the entire dashboard process for the build's duration.
Bonus: better error surfacing
Two of the three bugs were essentially silent, which made this take far longer to diagnose than it should have:
pty_ws in hermes_cli/web_server.py catches SystemExit and renders Chat unavailable: {exc} — which becomes the unhelpful Chat unavailable: 1. Surfacing the underlying npm/esbuild stderr (or at least a hint like "TUI build failed; see container logs") would have shaved hours off debugging.
The blank-canvas mode (bug Architecture planning #3) is the worst — the WebSocket connects, the PTY spawns, the node process runs, and nothing complains. There's no banner, no log line, no DevTools error. Adding a startup log from the TUI on whether ~/.hermes is writable, or a one-shot probe in _resolve_chat_argv that fails fast if HOME isn't writable for the current user, would catch this immediately.
Upstream issue draft — Hermes Docker image: Chat tab unusable due to four bugs
File this against
NousResearch/hermes-agentonce reviewed. Run from this directory:gh issue create --repo NousResearch/hermes-agent \ --title "Dashboard Chat tab unusable in Docker image — four compounding bugs (ui-tui chown, /opt/data file chown, HOME inheritance, ink-bundle staleness loop)" \ --body-file docs/upstream-issue-chat-tab-eacces.mdSummary
In the official
nousresearch/hermes-agent:latestDocker image (verified onv0.12.0 (2026.4.30)), the dashboard's embedded Chat tab (hermes dashboard --tui) is unusable out of the box due to four compounding bugs. Each one alone is enough to break the chat; fixing them in isolation produces a different failure mode each time, which made this take hours to diagnose.TL;DR of the four bugs, in the order they surface as you fix them:
/opt/hermes/ui-tui/is root-owned → TUI rebuild fails with EACCES → banner reads "Chat unavailable: 1"/opt/data/(notablyauth.jsonmode 0600) are root-owned → auth store unreadable → Models/Providers picker is empty andWARNING hermes_cli.auth: failed to parse /opt/data/auth.json [Errno 13]spams the logsgosupreservesHOME=/rootfrom Docker's default root start → TUI subprocess (node ui-tui/dist/entry.js) inheritsHOME=/root, can't write to~/.hermes, and silently produces no output — the Chat tab renders as a blank dark canvas with just a cursor blinking in the top-left corner_hermes_ink_bundle_stalelooks forink-bundle.jsbut the build producesentry-exports.js→ bundle is always considered stale → every Chat tab connection triggers a synchronousnpm run build && tsc && npm run build:compilechain on the asyncio event loop, blocking the dashboard for minutes per connection, while the rebuild never produces the file the check is looking for, so the next connection does the same thing again. WebSocket handshake to/api/ptytimes out beforeaccept()is reached.After all four are worked around, chat works correctly (WebSocket connects in < 1s, slash workers spawn, OpenRouter/DeepSeek calls succeed, ink TUI renders).
Bug #1 —
/opt/hermes/ui-tui/is root-ownedThe dashboard runs as the unprivileged
hermesuser (after the entrypoint drops privileges viagosu), but/opt/hermes/ui-tui/and itsdist/directories are baked into the image asroot:root. When_make_tui_argv()tries to rebuild the TUI bundle on first run, esbuild fails to write to/opt/hermes/ui-tui/packages/hermes-ink/dist/entry-exports.jswith EACCES, the build aborts,_make_tui_argvcallssys.exit(1), andweb_server.pty_wsreportsChat unavailable: {SystemExit(1)}over the WebSocket.Bug #2 —
/opt/datafiles are root-owned (entrypoint chown is non-recursive)The entrypoint's
needs_chowncheck only fires when the top-level$HERMES_HOMEdirectory has the wrong owner — root-owned files inside/opt/dataare left untouched. We hit this on/opt/data/auth.json(mode 0600), which made the auth store unreadable for thehermesuser:This warning repeats roughly once per second forever. Symptom in the dashboard: the Models / Providers picker is empty because the auth store has no readable entries (even though
OPENROUTER_API_KEYis set in the environment and the credential is reachable via env-pool fallback for direct CLI calls).Bug #3 —
gosupreserves Docker's defaultHOME=/rootThis was the killer. After fixing #1 and #2, the Chat tab still rendered blank — a dark canvas with just a cursor in the top-left, no spinner, no banner, no error.
Root cause: Docker starts the container as root by default, with
HOME=/rootfrom the env. When the entrypoint runsexec gosu hermes "$0" "$@", gosu by default preserves the parent's environment, includingHOME. So the gateway and dashboard processes run asuid=hermesbut withHOME=/root— a directory the hermes user has no read or write access to.The TUI subprocess (
node /opt/hermes/ui-tui/dist/entry.js) writes to~/.hermes/for skin caches, history, ink-cli state, etc. Every write fails silently, ink renders nothing, the WebSocket pumps an empty PTY stream, and the Chat tab shows a blank xterm.Verified by inspecting
/proc/<dashboard-pid>/environ:Workaround: set
HOME=/opt/data(the hermes user's actual passwd-listed homedir) in the container env. Then the chat works end-to-end.Bug #4 —
_hermes_ink_bundle_stalelooks for a file that never exists, triggering a synchronous rebuild on every chat connectionThis was the killer of killers. After working around bugs #1–#3, the Chat tab still hangs: the WebSocket to
/api/ptyopens a TCP connection, sends the upgrade request, and the server returns zero bytes — for minutes — until the browser times out.Root cause:
_make_tui_argvcalls_tui_build_needed→_hermes_ink_bundle_stale, which checks for/opt/hermes/ui-tui/packages/hermes-ink/dist/ink-bundle.js. That file does not ship in the image (onlyentry-exports.jsdoes, after bug #1 is worked around). So_hermes_ink_bundle_stalealways returnsTrue→_tui_build_neededalways returnsTrue→_make_tui_argvalways runssubprocess.run([npm, "run", "build"], capture_output=True)synchronously.That
subprocess.runblocks the asyncio event loop for the duration of the build (npm + tsc + esbuild bundling — multiple minutes). During that time, every HTTP request and WebSocket upgrade on the dashboard hangs. The browser hits its 10-second WebSocket open timeout long before the build finishes.To make matters worse: when the build does eventually complete, it still doesn't produce
ink-bundle.js(the build script targetsentry-exports.js). So the next connection starts the same rebuild from scratch.Verified by capturing the dashboard's child process tree mid-hang:
And by checking the file the staleness check is looking for:
Workaround: setting
HERMES_TUI_DIR=/opt/hermes/ui-tuiin the container env makes_make_tui_argvtake its prebuilt-bundle shortcut (which checksdist/entry.js, notink-bundle.js) and skip the rebuild path entirely. The chat then connects in < 1s.Reproduction
--tuimode:http://localhost:18789→ Chat tab.Confirmed root cause
Run
_make_tui_argvdirectly as thehermesuser:Output (abridged):
ls -la /opt/hermes/ui-tui/dist /opt/hermes/ui-tui/packages/hermes-ink/distshows both directories owned byroot:rootin the shipped image, while the running process isuid=hermes.Combined workaround (compose-level)
All three bugs can be papered over without touching the image, by:
HOME=/opt/datain the container envWrapper script (mounted at
/init-chown.sh):Compose snippet:
After all three workarounds are in place, the chat works end-to-end. Without all three, you get one of the three failure modes above (banner, empty picker, or blank canvas).
Suggested upstream fixes
Any of these would resolve the bugs they correspond to. Ideally all three areas get patched:
For bug #1 (ui-tui chown):
RUN chown -R hermes:hermes /opt/hermes/ui-tuiafter the build stage that producesdist/._tui_need_npm_install/_tui_build_neededso a fresh image with a complete prebuiltdist/doesn't trigger a rebuild attempt at all. Theentry.jsis already present and runnable; the rebuild is only needed in dev mode.For bug #2 (/opt/data file chown):
needs_chowncheck from a top-level stat to a recursive ownership check, OR just always runchown -R hermes:hermes "$HERMES_HOME"(cheap on a small data dir)./opt/dataships with the right ownership.For bug #3 (HOME inheritance):
--preserve-env=PATHetc. but explicitly resetHOMEbefore the gosu drop:gosu hermes -Hif a similar flag exists, orrunuser -l hermeswhich runs in a login shell and re-evaluatesHOMEfrom passwd.HOME=/opt/datain the Dockerfile viaENV HOME=/opt/data(simple, image-level fix).For bug #4 (ink-bundle staleness loop):
ink-bundle.js(or whatever name_hermes_ink_bundle_stalelooks for). The mismatch between the build output (entry-exports.js) and the staleness check looks like a rename that wasn't propagated._hermes_ink_bundle_staleto look at the file the build actually produces.ENV HERMES_TUI_DIR=/opt/hermes/ui-tuiin the Dockerfile so the prebuilt-bundle shortcut is used by default in the official image._resolve_chat_argvis called from insidepty_wsbeforeawait ws.accept(). If a build is genuinely needed, it should run viaasyncio.to_thread/loop.run_in_executor, with a "Building TUI bundle…" message streamed to the client. Today, every connection that triggers it freezes the entire dashboard process for the build's duration.Bonus: better error surfacing
Two of the three bugs were essentially silent, which made this take far longer to diagnose than it should have:
pty_wsinhermes_cli/web_server.pycatchesSystemExitand rendersChat unavailable: {exc}— which becomes the unhelpfulChat unavailable: 1. Surfacing the underlying npm/esbuild stderr (or at least a hint like "TUI build failed; see container logs") would have shaved hours off debugging.The blank-canvas mode (bug Architecture planning #3) is the worst — the WebSocket connects, the PTY spawns, the node process runs, and nothing complains. There's no banner, no log line, no DevTools error. Adding a startup log from the TUI on whether
~/.hermesis writable, or a one-shot probe in_resolve_chat_argvthat fails fast ifHOMEisn't writable for the current user, would catch this immediately.Environment
nousresearch/hermes-agent:latest(v0.12.0 (2026.4.30))hermes dashboard --tuiin foreground+background pattern (dashboard bg,hermes gateway runfg)Related code paths
_DASHBOARD_EMBEDDED_CHAT_ENABLEDgates/api/pty(works correctly)_make_tui_argvinhermes_cli/main.py(the failing path)pty_wsinhermes_cli/web_server.py(catchesSystemExitand returns the unhelpful: 1banner)