Bug Description
When terminal.backend: docker, Hermes file tools (write_file, patch, terminal) run inside the sandbox container per SKILL.md:997. The container's /root/.hermes is bind-mounted to a sandbox-local directory under …/profiles/<name>/sandboxes/docker/<task_id>/home, not the host profile's HERMES_HOME. Because the sandbox layout mirrors the host profile layout (the model sees a path like /root/.hermes/profiles/group1/SOUL.md), the agent treats the sandbox mirror as the authoritative file and writes succeed silently against a copy that is never read by the host process.
The host-side prompt assembly continues to load the real file via get_hermes_home() / "SOUL.md" (agent/prompt_builder.py:1326), so the agent reports "SOUL.md updated", subsequent sessions see no change, and on disk there are now two divergent SOUL.md files.
This is the natural neighbor of the cross-profile case fixed in #31290 — same class of confused-write, different axis (sandbox-mirror vs cross-profile). It is not covered by the guard added in #31290 (see "Why #31290 does not catch this" below).
Steps to Reproduce
On a deployment with terminal.backend: docker and container_persistent: true:
- Start an agent session under profile
group1 (Docker backend).
- Ask the agent to add a rule to
SOUL.md (e.g. "remember this: …").
- Agent calls
write_file / patch / terminal cat >> targeting what it sees as ~/.hermes/profiles/group1/SOUL.md.
- Tool reports success.
- Restart the session. New system prompt does not contain the rule.
Expected Behavior
One of:
- A: Agent has a host-side tool (
soul / profile_state) for editing authoritative profile state. File tools running inside the Docker sandbox refuse to write to paths matching authoritative profile state and tell the agent to use the host-side tool.
- B: Agent's system prompt / hermes-agent skill states clearly that under non-local
TERMINAL_ENV, /root/.hermes and ~/.hermes inside the sandbox are not the host profile, and listing the files that must not be edited from inside (SOUL.md, config.yaml, memories/*.md, cron/jobs.json, .env, auth.json).
Actual Behavior
Write lands in the sandbox mirror. Host file is untouched. Concrete evidence from a real deployment (hermes-agent v0.14.0 / 2026.5.16 / a91a57f):
$ find /home/hermes/.hermes -name SOUL.md -printf '%TY-%Tm-%Td %TH:%TM %u:%g %m %p\n' | sort
…
2026-05-25 16:34 hermes:hermes 600 /home/hermes/.hermes/profiles/group1/SOUL.md
2026-05-25 18:28 root:root 644 /home/hermes/.hermes/profiles/group1/sandboxes/docker/default/home/.hermes/profiles/group1/SOUL.md
- The 18:28 write is owned by
root:root because the container runs as root and the bind-mount maps /root → <sandbox>/home.
- The host file's mtime is earlier (16:34) — it was not touched by the 18:28 "successful write".
- Content check confirms: the rule the agent claimed to add appears only in the sandbox copy, not in the authoritative host file.
This is not a one-off. On the same host, additional sandbox-mirror SOUL.md files exist from prior incidents (May 7, separate path shape ~/.hermes/hermes-agent/docker/SOUL.md and ~/.hermes/profiles/group2/hermes-agent/docker/SOUL.md), suggesting the model has guessed several different "looks-like-profile" paths over time. Recurrence under default config indicates this affects every Docker-backend deployment, not a specific misconfiguration.
Root Cause Analysis
Three independent code locations form the failure mode:
tools/environments/docker.py:378-389 — persistent Docker sandbox binds <sandbox>/home to container /root. Profile state directory is intentionally not mounted.
agent/prompt_builder.py:1326 — SOUL.md is loaded host-side via get_hermes_home() / "SOUL.md", on the Python main process. The Docker container has no path to write through this code path.
SKILL.md:997 — already documents that under non-local TERMINAL_ENV, "every file tool (read_file, write_file, patch, search_files) runs inside the backend container, not on the host." But this single line is buried near line 1000 and does not name SOUL.md / config.yaml / memories / cron / .env as files that must not be edited from inside.
Net effect: the host designs an isolated sandbox (correct), but exposes path strings to the model that look identical between sandbox and host. There is no tool the agent can use to edit authoritative profile state from inside the sandbox, and no guard that catches the wrong attempt.
Why #31290 does not catch this
agent/file_safety.classify_cross_profile_target was added in #31290 and is a good start, but does not cover this case for three independent reasons:
PROFILE_SCOPED_AREAS = ("skills", "plugins", "cron", "memories") — top-level profile files (SOUL.md, config.yaml, .env, auth.json) are not in the list.
- The sandbox mirror path
…/profiles/group1/sandboxes/docker/default/home/.hermes/profiles/group1/SOUL.md has parts[2] == "sandboxes", which is not a scoped area, so the classifier returns None.
- Under Docker backend, the file-tool guard runs inside the container, where
_hermes_root_path() resolves to the sandbox /root/.hermes. The classifier's "world" is the sandbox itself, so it cannot detect that the target is sandbox-mirror state from the host's perspective.
Proposed Fix
Cheapest layer is a clear "Docker backend path boundary" section in skills/autonomous-ai-agents/hermes-agent/SKILL.md naming the authoritative profile files that must not be edited from inside a non-local terminal backend. Real fix is a host-side soul / profile_state tool operating on get_hermes_home() / "SOUL.md" from the Hermes main process (same shape as the existing memory tool), so the model has a correct destination instead of falling back to write_file / terminal. Defense-in-depth is extending classify_cross_profile_target to flag both top-level profile files (SOUL.md, config.yaml, .env, auth.json) and any …/sandboxes/<backend>/…/home/…hermes/… path as sandbox-mirror, ideally running the classifier host-side before backend dispatch. Explicitly not suggesting RW-mounting the host profile into the container — that exposes auth.json, .env, sessions/, state.db, cron/, hooks, and skill scripts to arbitrary shell commands and widens blast radius for a different problem.
Related
TEMPLATE FIELDS:
-
Affected Component: Tools (terminal, file ops, web, code execution, etc.); Agent Core (conversation loop, context compression, memory)
-
Operating System: Ubuntu 24.04 (host) / nikolaik/python-nodejs:python3.11-nodejs20 (Docker backend image)
-
Python Version: 3.11.15
-
Hermes Version: 0.14.0 (2026.5.16) — commit a91a57fa. Verified that the issue is not fixed in main (4c64638): tools/environments/docker.py lines 378-389 unchanged since v0.14.0; SKILL.md still does not name SOUL.md/config/memories as Docker-backend-unsafe.
-
Debug Report: Omitting hermes debug share paste links from the public issue to avoid exposing deployment-specific endpoints and conversation paths. Key fields from the dump, scrubbed:
version: 0.14.0 (2026.5.16) [a91a57fa]
os: Linux 6.8.0 x86_64
python: 3.11.15
openai_sdk: 2.33.0
hermes_home: ~/.hermes
terminal: docker (under a profile we'll call "group1"; config below)
memory: built-in
mcp_servers: 0
-
PR willingness: No
terminal config used (group1 profile):
terminal:
backend: docker
container_persistent: true
docker_image: nikolaik/python-nodejs:python3.11-nodejs20
docker_volumes:
- /srv/hermes/groups/group1/workspace:/workspace
- /srv/hermes/groups/group1/output:/output
- /srv/hermes/groups/group1/vault:/vault
- /srv/hermes/shared/readonly:/shared:ro
- /home/hermes/.hermes/profiles/group1/.env:/root/.hermes/.env:ro
docker_mount_cwd_to_workspace: false
docker_run_as_host_user: false
Reported-by: Diandian
Bug Description
When
terminal.backend: docker, Hermes file tools (write_file,patch,terminal) run inside the sandbox container perSKILL.md:997. The container's/root/.hermesis bind-mounted to a sandbox-local directory under…/profiles/<name>/sandboxes/docker/<task_id>/home, not the host profile'sHERMES_HOME. Because the sandbox layout mirrors the host profile layout (the model sees a path like/root/.hermes/profiles/group1/SOUL.md), the agent treats the sandbox mirror as the authoritative file and writes succeed silently against a copy that is never read by the host process.The host-side prompt assembly continues to load the real file via
get_hermes_home() / "SOUL.md"(agent/prompt_builder.py:1326), so the agent reports "SOUL.md updated", subsequent sessions see no change, and on disk there are now two divergent SOUL.md files.This is the natural neighbor of the cross-profile case fixed in #31290 — same class of confused-write, different axis (sandbox-mirror vs cross-profile). It is not covered by the guard added in #31290 (see "Why #31290 does not catch this" below).
Steps to Reproduce
On a deployment with
terminal.backend: dockerandcontainer_persistent: true:group1(Docker backend).SOUL.md(e.g. "remember this: …").write_file/patch/ terminalcat >>targeting what it sees as~/.hermes/profiles/group1/SOUL.md.Expected Behavior
One of:
soul/profile_state) for editing authoritative profile state. File tools running inside the Docker sandbox refuse to write to paths matching authoritative profile state and tell the agent to use the host-side tool.TERMINAL_ENV,/root/.hermesand~/.hermesinside the sandbox are not the host profile, and listing the files that must not be edited from inside (SOUL.md, config.yaml, memories/*.md, cron/jobs.json, .env, auth.json).Actual Behavior
Write lands in the sandbox mirror. Host file is untouched. Concrete evidence from a real deployment (hermes-agent v0.14.0 / 2026.5.16 / a91a57f):
root:rootbecause the container runs as root and the bind-mount maps/root→<sandbox>/home.This is not a one-off. On the same host, additional sandbox-mirror SOUL.md files exist from prior incidents (May 7, separate path shape
~/.hermes/hermes-agent/docker/SOUL.mdand~/.hermes/profiles/group2/hermes-agent/docker/SOUL.md), suggesting the model has guessed several different "looks-like-profile" paths over time. Recurrence under default config indicates this affects every Docker-backend deployment, not a specific misconfiguration.Root Cause Analysis
Three independent code locations form the failure mode:
tools/environments/docker.py:378-389— persistent Docker sandbox binds<sandbox>/hometo container/root. Profile state directory is intentionally not mounted.agent/prompt_builder.py:1326— SOUL.md is loaded host-side viaget_hermes_home() / "SOUL.md", on the Python main process. The Docker container has no path to write through this code path.SKILL.md:997— already documents that under non-localTERMINAL_ENV, "every file tool (read_file,write_file,patch,search_files) runs inside the backend container, not on the host." But this single line is buried near line 1000 and does not name SOUL.md / config.yaml / memories / cron / .env as files that must not be edited from inside.Net effect: the host designs an isolated sandbox (correct), but exposes path strings to the model that look identical between sandbox and host. There is no tool the agent can use to edit authoritative profile state from inside the sandbox, and no guard that catches the wrong attempt.
Why #31290 does not catch this
agent/file_safety.classify_cross_profile_targetwas added in #31290 and is a good start, but does not cover this case for three independent reasons:PROFILE_SCOPED_AREAS = ("skills", "plugins", "cron", "memories")— top-level profile files (SOUL.md,config.yaml,.env,auth.json) are not in the list.…/profiles/group1/sandboxes/docker/default/home/.hermes/profiles/group1/SOUL.mdhasparts[2] == "sandboxes", which is not a scoped area, so the classifier returnsNone._hermes_root_path()resolves to the sandbox/root/.hermes. The classifier's "world" is the sandbox itself, so it cannot detect that the target is sandbox-mirror state from the host's perspective.Proposed Fix
Cheapest layer is a clear "Docker backend path boundary" section in
skills/autonomous-ai-agents/hermes-agent/SKILL.mdnaming the authoritative profile files that must not be edited from inside a non-local terminal backend. Real fix is a host-sidesoul/profile_statetool operating onget_hermes_home() / "SOUL.md"from the Hermes main process (same shape as the existingmemorytool), so the model has a correct destination instead of falling back towrite_file/terminal. Defense-in-depth is extendingclassify_cross_profile_targetto flag both top-level profile files (SOUL.md,config.yaml,.env,auth.json) and any…/sandboxes/<backend>/…/home/…hermes/…path as sandbox-mirror, ideally running the classifier host-side before backend dispatch. Explicitly not suggesting RW-mounting the host profile into the container — that exposesauth.json,.env,sessions/,state.db,cron/, hooks, and skill scripts to arbitrary shell commands and widens blast radius for a different problem.Related
~/.hermes" confusion, different layer.TEMPLATE FIELDS:
Affected Component: Tools (terminal, file ops, web, code execution, etc.); Agent Core (conversation loop, context compression, memory)
Operating System: Ubuntu 24.04 (host) /
nikolaik/python-nodejs:python3.11-nodejs20(Docker backend image)Python Version: 3.11.15
Hermes Version: 0.14.0 (2026.5.16) — commit
a91a57fa. Verified that the issue is not fixed inmain(4c64638):tools/environments/docker.pylines 378-389 unchanged since v0.14.0;SKILL.mdstill does not name SOUL.md/config/memories as Docker-backend-unsafe.Debug Report: Omitting
hermes debug sharepaste links from the public issue to avoid exposing deployment-specific endpoints and conversation paths. Key fields from the dump, scrubbed:PR willingness: No
terminal config used (group1 profile):
Reported-by: Diandian