hermes - 💡(How to fix) Fix [Bug]: Docker terminal backend lets file tools write to a sandbox-mirror copy of authoritative profile state (SOUL.md, etc.) — follow-up to #31290

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

Three independent code locations form the failure mode:

  1. tools/environments/docker.py:378-389 — persistent Docker sandbox binds <sandbox>/home to container /root. Profile state directory is intentionally not mounted.
  2. 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.
  3. 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.

Fix Action

Fix / Workaround

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.

  1. Start an agent session under profile group1 (Docker backend).

  2. Ask the agent to add a rule to SOUL.md (e.g. "remember this: …").

  3. Agent calls write_file / patch / terminal cat >> targeting what it sees as ~/.hermes/profiles/group1/SOUL.md.

  4. Tool reports success.

  5. Restart the session. New system prompt does not contain the rule.

  6. tools/environments/docker.py:378-389 — persistent Docker sandbox binds <sandbox>/home to container /root. Profile state directory is intentionally not mounted.

  7. 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.

  8. 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.

Code Example

$ 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

---

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

---

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
RAW_BUFFERClick to expand / collapse

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:

  1. Start an agent session under profile group1 (Docker backend).
  2. Ask the agent to add a rule to SOUL.md (e.g. "remember this: …").
  3. Agent calls write_file / patch / terminal cat >> targeting what it sees as ~/.hermes/profiles/group1/SOUL.md.
  4. Tool reports success.
  5. 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 / a91a57fa):

$ 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:

  1. tools/environments/docker.py:378-389 — persistent Docker sandbox binds <sandbox>/home to container /root. Profile state directory is intentionally not mounted.
  2. 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.
  3. 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:

  1. PROFILE_SCOPED_AREAS = ("skills", "plugins", "cron", "memories") — top-level profile files (SOUL.md, config.yaml, .env, auth.json) are not in the list.
  2. 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.
  3. 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

  • #31290 — cross-profile soft guard on file-write tools (merged 2026-05-24). This issue is the sandbox-mirror sibling of the case #31290 fixed.
  • #12582 — interactive CLI vs managed runtime split-state (open). Same class of "two parallel ~/.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 in main (4c6463889): 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

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING