# NousResearch/hermes-agent · Docker backend CWD-to-workspace mounting behavior

## Open now

- Nothing open on GitHub.

These issues and PRs concern whether and how the host current working directory is automatically mounted to /workspace in the Docker backend, including debate over making it automatic versus an explicit opt-in.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #1534. Verify it addresses your case.
- **Recommended:** read PR #1534 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.40`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #1445: \[Bug\]: Hermes does not map current directory into docker backend automatically.

## Prior & competing PRs

- **#1534: fix(docker): make cwd workspace mount explicit opt-in**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli-config.yaml.example`, `cli.py`, `hermes_cli/config.py`, `tests/hermes_cli/test_set_config_value.py`, `tests/tools/test_docker_environment.py`, `tests/tools/test_modal_sandbox_fixes.py`, `tools/environments/docker.py`, `tools/file_tools.py`, `tools/terminal_tool.py`, `website/docs/reference/environment-variables.md`, `website/docs/user-guide/configuration.md`
  - fix hunks:
  - `cli-config.yaml.example`

    ```diff
    @@ -76,8 +76,9 @@ model:
     #   - Messaging (Telegram/Discord): Uses MESSAGING_CWD from .env (default: home)
     terminal:
       backend: "local"
    -  cwd: "."  # For local backend: "." = current directory. Ignored for remote backends.
    +  cwd: "."  # For local backend: "." = current directory. Ignored for remote backends unless a backend documents otherwise.
       timeout: 180
    +  docker_mount_cwd_to_workspace: false  # SECURITY: off by default. Opt in to mount the launch cwd into Docker /workspace.
       lifetime_seconds: 300
       # sudo_password: ""  # Enable sudo commands (pipes via sudo -S) - SECURITY WARNING: plaintext!
     
    ```

    ```diff
    @@ -107,6 +108,7 @@ terminal:
     #   timeout: 180
     #   lifetime_seconds: 300
     #   docker_image: "nikolaik/python-nodejs:python3.11-nodejs20"
    +#   docker_mount_cwd_to_workspace: true   # Explicit opt-in: mount your launch cwd into /workspace
     
     # -----------------------------------------------------------------------------
     # OPTION 4: Singularity/Apptainer container
    ```
  - `cli.py`

    ```diff
    @@ -165,6 +165,7 @@ def load_cli_config() -> Dict[str, Any]:
                 "modal_image": "python:3.11",
                 "daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20",
                 "docker_volumes": [],  # host:container volume mounts for Docker backend
    +            "docker_mount_cwd_to_workspace": False,  # explicit opt-in only; default off for sandbox isolation
             },
             "browser": {
                 "inactivity_timeout": 120,  # Auto-cleanup inactive browser sessions after 2 min
    ```

    ```diff
    @@ -330,6 +331,7 @@ def load_cli_config() -> Dict[str, Any]:
             "container_disk": "TERMINAL_CONTAINER_DISK",
             "container_persistent": "TERMINAL_CONTAINER_PERSISTENT",
             "docker_volumes": "TERMINAL_DOCKER_VOLUMES",
    +        "docker_mount_cwd_to_workspace": "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE",
             "sandbox_dir": "TERMINAL_SANDBOX_DIR",
             # Persistent shell (non-local backends)
             "persistent_shell": "TERMINAL_PERSISTENT_SHELL",
    ```
  - `hermes_cli/config.py`

    ```diff
    @@ -118,6 +118,9 @@ def ensure_hermes_home():
             # Each entry is "host_path:container_path" (standard Docker -v syntax).
             # Example: ["/home/user/projects:/workspace/projects", "/data:/data"]
             "docker_volumes": [],
    +        # Explicit opt-in: mount the host cwd into /workspace for Docker sessions.
    +        # Default off because passing host directories into a sandbox weakens isolation.
    +        "docker_mount_cwd_to_workspace": False,
             # Persistent shell — keep a long-lived bash shell across execute() calls
             # so cwd/env vars/shell variables survive between commands.
             # Enabled by default for non-local backends (SSH); local is always opt-in
    ```

    ```diff
    @@ -1407,6 +1410,7 @@ def set_config_value(key: str, value: str):
             "terminal.singularity_image": "TERMINAL_SINGULARITY_IMAGE",
             "terminal.modal_image": "TERMINAL_MODAL_IMAGE",
             "terminal.daytona_image": "TERMINAL_DAYTONA_IMAGE",
    +        "terminal.docker_mount_cwd_to_workspace": "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE",
             "terminal.cwd": "TERMINAL_CWD",
             "terminal.timeout": "TERMINAL_TIMEOUT",
             "terminal.sandbox_dir": "TERMINAL_SANDBOX_DIR",
    ```
  - `tests/hermes_cli/test_set_config_value.py`

    ```diff
    @@ -115,3 +115,13 @@ def test_terminal_image_goes_to_config(self, _isolated_hermes_home):
             set_config_value("terminal.docker_image", "python:3.12")
             config = _read_config(_isolated_hermes_home)
             assert "python:3.12" in config
    +
    +    def test_terminal_docker_cwd_mount_flag_goes_to_config_and_env(self, _isolated_hermes_home):
    +        set_config_value("terminal.docker_mount_cwd_to_workspace", "true")
    +        config = _read_config(_isolated_hermes_home)
    +        env_content = _read_env(_isolated_hermes_home)
    +        assert "docker_mount_cwd_to_workspace: 'true'" in config or "docker_mount_cwd_to_workspace: true" in config
    +        assert (
    +            "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE=true" in env_content
    +            or "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE=True" in env_content
    +        )
    ```
  - `tests/tools/test_docker_environment.py`

    ```diff
    @@ -1,11 +1,31 @@
     import logging
     import subprocess
    +import sys
    +import types
     
     import pytest
     
     from tools.environments import docker as docker_env
     
     
    +def _install_fake_minisweagent(monkeypatch, captured_run_args):
    +    class MockInnerDocker:
    +        container_id = "fake-container"
    +        config = type("Config", (), {"executable": "/usr/bin/docker", "forward_env": [], "env": {}})()
    +
    +        def __init__(self, **kwargs):
    +            captured_run_args.extend(kwargs.get("run_args", []))
    +
    +    minisweagent_mod = types.ModuleType("minisweagent")
    +    environments_mod = types.ModuleType("minisweagent.environments")
    +    docker_mod = types.ModuleType("minisweagent.environments.docker")
    +    docker_mod.DockerEnvironment = MockInnerDocker
    +
    +    monkeypatch.setitem(sys.modules, "minisweagent", minisweagent_mod)
    +    monkeypatch.setitem(sys.modules, "minisweagent.environments", environments_mod)
    +    monkeypatch.setitem(sys.modules, "minisweagent.environments.docker", docker_mod)
    +
    +
     def _make_dummy_env(**kwargs):
         """Helper to construct DockerEnvironment with minimal required args."""
         return docker_env.DockerEnvironment(
    ```

    ```diff
    @@ -19,6 +39,8 @@ def _make_dummy_env(**kwargs):
             task_id=kwargs.get("task_id", "test-task"),
             volumes=kwargs.get("volumes", []),
             network=kwargs.get("network", True),
    +        host_cwd=kwargs.get("host_cwd"),
    +        auto_mount_cwd=kwargs.get("auto_mount_cwd", False),
         )
     
     
    ```

    ```diff
    @@ -86,3 +108,106 @@ def _run(cmd, **kwargs):
             })
         ]
     
    +
    +def test_auto_mount_host_cwd_adds_volume(monkeypatch, tmp_path):
    +    """Opt-in docker cwd mounting should bind the host cwd to /workspace."""
    +    project_dir = tmp_path / "my-project"
    +    project_dir.mkdir()
    +
    +    def _run_docker_version(*args, **kwargs):
    +        return subprocess.CompletedProcess(args[0], 0, stdout="Docker version", stderr="")
    +
    +    monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
    +    monkeypatch.setattr(docker_env.subprocess, "run", _run_docker_version)
    +
    +    captured_run_args = []
    +    _install_fake_minisweagent(monkeypatch, captured_run_args)
    +
    +    _make_dummy_env(
    +        cwd="/workspace",
    +        host_cwd=str(project_dir),
    +        auto_mount_cwd=True,
    +    )
    +
    +    run_args_str = " ".join(captured_run_args)
    +    assert f"{project_dir}:/workspace" in run_args_str
    +
    +
    +def test_auto_mount_disabled_by_default(monkeypatch, tmp_path):
    +    """Host cwd should not be mounted unless the caller explicitly opts in."""
    +    project_dir = tmp_path / "my-project"
    +    project_dir.mkdir()
    +
    +    def _run_docker_version(*args, **kwargs):
    +        return subprocess.CompletedProcess(args[0], 0, stdout="Docker version", stderr="")
    +
    +    monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
    +    monkeypatch.setattr(docker_env.subprocess, "run", _run_docker_version)
    +
    +    captured_run_args = []
    +    _install_fake_minisweagent(monkeypatch, captured_run_args)
    +
    +    _make_dummy_env(
    +        cwd="/root",
    +        host_cwd=str(project_dir),
    +        auto_mount_cwd=False,
    +    )
    +
    +    run_args_str = " ".join(captured_run_args)
    +    assert f"{project_dir}:/workspace" not in run_args_str
    +
    +
    +def test_auto_mount_skipped_when_workspace_already_mounted(monkeypatch, tmp_path):
    +    """Explicit user volumes for /workspace should take precedence over cwd mount."""
    +    project_dir = tmp_path / "my-project"
    +    project_dir.mkdir()
    +    other_dir = tmp_path / "other"
    +    other_dir.mkdir()
    +
    +    def _run_docker_version(*args, **kwargs):
    +        return subprocess.CompletedProcess(args[0], 0, stdout="Docker version", stderr="")
    +
    +    monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
    +    monkeypatch.setattr(docker_env.subprocess, "run", _run_docker_version)
    +
    +    captured_run_args = []
    +    _install_fake_minisweagent(monkeypatch, captured_run_args)
    +
    +    _make_dummy_env(
    +        cwd="/workspace",
    +        host_cwd=str(project_dir),
    +        auto_mount_cwd=True,
    +        volumes=[f"{other_dir}:/workspace"],
    +    )
    +
    +    run_args_str = " ".join(captured_run_args)
    +    assert f"{other_dir}:/workspace" in run_args_str
    +    assert run_args_str.count(":/workspace") == 1
    +
    +
    +def test_auto_mount_replaces_persistent_workspace_bind(monkeypatch, tmp_path):
    +    """Persistent mode should still prefer the configured host cwd at /workspace."""
    +    project_dir = tmp_path / "my-project"
    +    project_dir.mkdir()
    +
    +    def _run_docker_version(*args, **kwargs):
    +        return subprocess.CompletedProcess(args[0], 0, stdout="Docker version", stderr="")
    +
    +    monkeypatch.setattr(docker_env, "find_docker", lambda: "/usr/bin/docker")
    +    monkeypatch.setattr(docker_env.subprocess, "run", _run_docker_version)
    +
    +    captured_run_args = []
    +    _install_fake_minisweagent(monkeypatch, captured_run_args)
    +
    +    _make_dummy_env(
    +        cwd="/workspace",
    +        persistent_filesystem=True,
    +        host_cwd=str(project_dir),
    +        auto_mount_cwd=True,
    +        task_id="test-persistent-auto-mount",
    +    )
    +
    +    run_args_str = " ".join(captured_run_args)
    +    assert f"{project_dir}:/workspace" in run_args_str
    +    assert "/sandboxes/docker/test-persistent-auto-mount/workspace:/workspace" not in run_args_str
    +
    ```
  - `tests/tools/test_modal_sandbox_fixes.py`

    ```diff
    @@ -91,8 +91,8 @@ def test_home_path_replaced_for_modal(self):
                     "/home/ paths should be replaced for modal backend."
                 )
     
    -    def test_users_path_replaced_for_docker(self):
    -        """TERMINAL_CWD=/Users/... should be replaced with /root for docker."""
    +    def test_users_path_replaced_for_docker_by_default(self):
    +        """Docker should keep host paths out of the sandbox unless explicitly enabled."""
             with patch.dict(os.environ, {
                 "TERMINAL_ENV": "docker",
                 "TERMINAL_CWD": "/Users/someone/projects",
    ```

    ```diff
    @@ -100,8 +100,22 @@ def test_users_path_replaced_for_docker(self):
                 config = _tt_mod._get_env_config()
                 assert config["cwd"] == "/root", (
                     f"Expected /root, got {config['cwd']}. "
    -                "/Users/ paths should be replaced for docker backend."
    +                "Host paths should be discarded for docker backend by default."
                 )
    +            assert config["host_cwd"] is None
    +            assert config["docker_mount_cwd_to_workspace"] is False
    +
    +    def test_users_path_maps_to_workspace_for_docker_when_enabled(self):
    +        """Docker should map the host cwd into /workspace only when explicitly enabled."""
    +        with patch.dict(os.environ, {
    +            "TERMINAL_ENV": "docker",
    +            "TERMINAL_CWD": "/Users/someone/projects",
    +            "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE": "true",
    +        }):
    +            config = _tt_mod._get_env_config()
    +            assert config["cwd"] == "/workspace"
    +            assert config["host_cwd"] == "/Users/someone/projects"
    +            assert config["docker_mount_cwd_to_workspace"] is True
     
         def test_windows_path_replaced_for_modal(self):
             """TERMINAL_CWD=C:\\Users\\... should be replaced for modal."""
    ```

    ```diff
    @@ -119,12 +133,27 @@ def test_default_cwd_is_root_for_container_backends(self):
                     # Remove TERMINAL_CWD so it uses default
                     env = os.environ.copy()
                     env.pop("TERMINAL_CWD", None)
    +                env.pop("TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE", None)
                     with patch.dict(os.environ, env, clear=True):
                         config = _tt_mod._get_env_config()
                         assert config["cwd"] == "/root", (
                             f"Backend {backend}: expected /root default, got {config['cwd']}"
                         )
     
    +    def test_docker_default_cwd_maps_current_directory_when_enabled(self):
    +        """Docker should use /workspace when cwd mounting is explicitly enabled."""
    +        with patch("tools.terminal_tool.os.getcwd", return_value="/home/user/project"):
    +            with patch.dict(os.environ, {
    +                "TERMINAL_ENV": "docker",
    +                "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE": "true",
    +            }, clear=False):
    +                env = os.environ.copy()
    +                env.pop("TERMINAL_CWD", None)
    +                with patch.dict(os.environ, env, clear=True):
    +                    config = _tt_mod._get_env_config()
    +                    assert config["cwd"] == "/workspace"
    +                    assert config["host_cwd"] == "/home/user/project"
    +
         def test_local_backend_uses_getcwd(self):
             """Local backend should use os.getcwd(), not /root."""
             with patch.dict(os.environ, {"TERMINAL_ENV": "local"}, clear=False):
    ```

    ```diff
    @@ -134,6 +163,31 @@ def test_local_backend_uses_getcwd(self):
                     config = _tt_mod._get_env_config()
                     assert config["cwd"] == os.getcwd()
     
    +    def test_create_environment_passes_docker_host_cwd_and_flag(self, monkeypatch):
    +        """Docker host cwd and mount flag should reach DockerEnvironment."""
    +        captured = {}
    +        sentinel = object()
    +
    +        def _fake_docker_environment(**kwargs):
    +            captured.update(kwargs)
    +            return sentinel
    +
    +        monkeypatch.setattr(_tt_mod, "_DockerEnvironment", _fake_docker_environment)
    +
    +        env = _tt_mod._create_environment(
    +            env_type="docker",
    +            image="python:3.11",
    +            cwd="/workspace",
    +            timeout=60,
    +            container_config={"docker_mount_cwd_to_workspace": True},
    +            host_cwd="/home/user/project",
    +        )
    +
    +        assert env is sentinel
    +        assert captured["cwd"] == "/workspace"
    +        assert captured["host_cwd"] == "/home/user/project"
    +        assert captured["auto_mount_cwd"] is True
    +
         def test_ssh_preserves_home_paths(self):
             """SSH backend should NOT replace /home/ paths (they're valid remotely)."""
             with patch.dict(os.environ, {
    ```
  - `tools/environments/docker.py`

    ```diff
    @@ -172,6 +172,8 @@ def __init__(
             task_id: str = "default",
             volumes: list = None,
             network: bool = True,
    +        host_cwd: str = None,
    +        auto_mount_cwd: bool = False,
         ):
             if cwd == "~":
                 cwd = "/root"
    ```

    ```diff
    @@ -214,30 +216,9 @@ def __init__(
             # mode uses tmpfs (ephemeral, fast, gone on cleanup).
             from tools.environments.base import get_sandbox_dir
     
    -        self._workspace_dir: Optional[str] = None
    -        self._home_dir: Optional[str] = None
    -        if self._persistent:
    -            sandbox = get_sandbox_dir() / "docker" / task_id
    -            self._workspace_dir = str(sandbox / "workspace")
    -            self._home_dir = str(sandbox / "home")
    -            os.makedirs(self._workspace_dir, exist_ok=True)
    -            os.makedirs(self._home_dir, exist_ok=True)
    -            writable_args = [
    -                "-v", f"{self._workspace_dir}:/workspace",
    -                "-v", f"{self._home_dir}:/root",
    -            ]
    -        else:
    -            writable_args = [
    -                "--tmpfs", "/workspace:rw,exec,size=10g",
    -                "--tmpfs", "/home:rw,exec,size=1g",
    -                "--tmpfs", "/root:rw,exec,size=1g",
    -            ]
    -
    -        # All containers get security hardening (capabilities dropped, no privilege
    -        # escalation, PID limits). The container filesystem is writable so agents
    -        # can install packages as needed.
             # User-configured volume mounts (from config.yaml docker_volumes)
             volume_args = []
    +        workspace_explicitly_mounted = False
             for vol in (volumes or []):
                 if not isinstance(vol, str):
                     logger.warning(f"Docker volume entry is not a string: {vol!r}")
    ```

    ```diff
    @@ -247,9 +228,53 @@ def __init__(
                     continue
                 if ":" in vol:
                     volume_args.extend(["-v", vol])
    +                if ":/workspace" in vol:
    +                    workspace_explicitly_mounted = True
                 else:
                     logger.warning(f"Docker volume '{vol}' missing colon, skipping")
     
    +        host_cwd_abs = os.path.abspath(os.path.expanduser(host_cwd)) if host_cwd else ""
    +        bind_host_cwd = (
    +            auto_mount_cwd
    +            and bool(host_cwd_abs)
    +            and os.path.isdir(host_cwd_abs)
    +            and not workspace_explicitly_mounted
    +        )
    +        if auto_mount_cwd and host_cwd and not os.path.isdir(host_cwd_abs):
    +            logger.debug(f"Skipping docker cwd mount: host_cwd is not a valid directory: {host_cwd}")
    +
    +        self._workspace_dir: Optional[str] = None
    +        self._home_dir: Optional[str] = None
    +        writable_args = []
    +        if self._persistent:
    +            sandbox = get_sandbox_dir() / "docker" / task_id
    +            self._home_dir = str(sandbox / "home")
    +            os.makedirs(self._home_dir, exist_ok=True)
    +            writable_args.extend([
    +                "-v", f"{self._home_dir}:/root",
    +            ])
    +            if not bind_host_cwd and not workspace_explicitly_mounted:
    +                self._workspace_dir = str(sandbox / "workspace")
    +                os.makedirs(self._workspace_dir, exist_ok=True)
    +                writable_args.extend([
    +                    "-v", f"{self._workspace_dir}:/workspace",
    +                ])
    +        else:
    +            if not bind_host_cwd and not workspace_explicitly_mounted:
    +                writable_args.extend([
    +                    "--tmpfs", "/workspace:rw,exec,size=10g",
    +                ])
    +            writable_args.extend([
    +                "--tmpfs", "/home:rw,exec,size=1g",
    +                "--tmpfs", "/root:rw,exec,size=1g",
    +            ])
    +
    +        if bind_host_cwd:
    +            logger.info(f"Mounting configured host cwd to /workspace: {host_cwd_abs}")
    +            volume_args = ["-v", f"{host_cwd_abs}:/workspace", *volume_args]
    +        elif workspace_explicitly_mounted:
    +            logger.debug("Skipping docker cwd mount: /workspace already mounted by user config")
    +
             logger.info(f"Docker volume_args: {volume_args}")
             all_run_args = list(_SECURITY_ARGS) + writable_args + resource_args + volume_args
             logger.info(f"Docker run_args: {all_run_args}")
    ```
  - `tools/file_tools.py`

    ```diff
    @@ -140,6 +140,7 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
                     container_config=container_config,
                     local_config=local_config,
                     task_id=task_id,
    +                host_cwd=config.get("host_cwd"),
                 )
     
                 with _env_lock:
    ```
  - `tools/terminal_tool.py`

    ```diff
    @@ -466,6 +466,8 @@ def _get_env_config() -> Dict[str, Any]:
         default_image = "nikolaik/python-nodejs:python3.11-nodejs20"
         env_type = os.getenv("TERMINAL_ENV", "local")
         
    +    mount_docker_cwd = os.getenv("TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE", "false").lower() in ("true", "1", "yes")
    +
         # Default cwd: local uses the host's current directory, everything
         # else starts in the user's home (~ resolves to whatever account
         # is running inside the container/remote).
    ```

    ```diff
    @@ -475,16 +477,25 @@ def _get_env_config() -> Dict[str, Any]:
             default_cwd = "~"
         else:
             default_cwd = "/root"
    -    
    +
         # Read TERMINAL_CWD but sanity-check it for container backends.
    -    # If the CWD looks like a host-local path that can't exist inside a
    -    # container/sandbox, fall back to the backend's own default. This
    -    # catches the case where cli.py (or .env) leaked the host's CWD.
    -    # SSH is excluded since /home/ paths are valid on remote machines.
    +    # If Docker cwd passthrough is explicitly enabled, remap the host path to
    +    # /workspace and track the original host path separately. Otherwise keep the
    +    # normal sandbox behavior and discard host paths.
         cwd = os.getenv("TERMINAL_CWD", default_cwd)
    -    if env_type in ("modal", "docker", "singularity", "daytona") and cwd:
    +    host_cwd = None
    +    host_prefixes = ("/Users/", "/home/", "C:\\", "C:/")
    +    if env_type == "docker" and mount_docker_cwd:
    +        docker_cwd_source = os.getenv("TERMINAL_CWD") or os.getcwd()
    +        candidate = os.path.abspath(os.path.expanduser(docker_cwd_source))
    +        if (
    +            any(candidate.startswith(p) for p in host_prefixes)
    +            or (os.path.isabs(candidate) and os.path.isdir(candidate) and not candidate.startswith(("/workspace", "/root")))
    +        ):
    +            host_cwd = candidate
    +            cwd = "/workspace"
    +    elif env_type in ("modal", "docker", "singularity", "daytona") and cwd:
             # Host paths that won't exist inside containers
    -        host_prefixes = ("/Users/", "/home/", "C:\\", "C:/")
             if any(cwd.startswith(p) for p in host_prefixes) and cwd != default_cwd:
                 logger.info("Ignoring TERMINAL_CWD=%r for %s backend "
                             "(host path won't exist in sandbox). Using %r instead.",
    ```

    ```diff
    @@ -498,6 +509,8 @@ def _get_env_config() -> Dict[str, Any]:
             "modal_image": os.getenv("TERMINAL_MODAL_IMAGE", default_image),
             "daytona_image": os.getenv("TERMINAL_DAYTONA_IMAGE", default_image),
             "cwd": cwd,
    +        "host_cwd": host_cwd,
    +        "docker_mount_cwd_to_workspace": mount_docker_cwd,
             "timeout": _parse_env_var("TERMINAL_TIMEOUT", "180"),
             "lifetime_seconds": _parse_env_var("TERMINAL_LIFETIME_SECONDS", "300"),
             # SSH-specific config
    ```

    ```diff
    @@ -525,7 +538,8 @@ def _get_env_config() -> Dict[str, Any]:
     def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
                             ssh_config: dict = None, container_config: dict = None,
                             local_config: dict = None,
    -                        task_id: str = "default"):
    +                        task_id: str = "default",
    +                        host_cwd: str = None):
         """
         Create an execution environment from mini-swe-agent.
         
    ```

    ```diff
    @@ -537,6 +551,7 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
             ssh_config: SSH connection config (for env_type="ssh")
             container_config: Resource config for container backends (cpu, memory, disk, persistent)
             task_id: Task identifier for environment reuse and snapshot keying
    +        host_cwd: Optional host working directory to bind into Docker when explicitly enabled
             
         Returns:
             Environment instance with execute() method
    ```

    ```diff
    @@ -559,6 +574,8 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
                 cpu=cpu, memory=memory, disk=disk,
                 persistent_filesystem=persistent, task_id=task_id,
                 volumes=volumes,
    +            host_cwd=host_cwd,
    +            auto_mount_cwd=cc.get("docker_mount_cwd_to_workspace", False),
             )
         
         elif env_type == "singularity":
    ```

    ```diff
    @@ -948,6 +965,7 @@ def terminal_tool(
                                     "container_disk": config.get("container_disk", 51200),
                                     "container_persistent": config.get("container_persistent", True),
                                     "docker_volumes": config.get("docker_volumes", []),
    +                                "docker_mount_cwd_to_workspace": config.get("docker_mount_cwd_to_workspace", False),
                                 }
     
                             local_config = None
    ```

    ```diff
    @@ -965,6 +983,7 @@ def terminal_tool(
                                 container_config=container_config,
                                 local_config=local_config,
                                 task_id=effective_task_id,
    +                            host_cwd=config.get("host_cwd"),
                             )
                         except ImportError as e:
                             return json.dumps({
    ```
  - `website/docs/reference/environment-variables.md`

    ```diff
    @@ -79,6 +79,7 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe
     | `TERMINAL_ENV` | Backend: `local`, `docker`, `ssh`, `singularity`, `modal`, `daytona` |
     | `TERMINAL_DOCKER_IMAGE` | Docker image (default: `python:3.11`) |
     | `TERMINAL_DOCKER_VOLUMES` | Additional Docker volume mounts (comma-separated `host:container` pairs) |
    +| `TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE` | Advanced opt-in: mount the launch cwd into Docker `/workspace` (`true`/`false`, default: `false`) |
     | `TERMINAL_SINGULARITY_IMAGE` | Singularity image or `.sif` path |
     | `TERMINAL_MODAL_IMAGE` | Modal container image |
     | `TERMINAL_DAYTONA_IMAGE` | Daytona sandbox image |
    ```
  - `website/docs/user-guide/configuration.md`

    ```diff
    @@ -453,7 +453,8 @@ terminal:
     
       # Docker-specific settings
       docker_image: "nikolaik/python-nodejs:python3.11-nodejs20"
    -  docker_volumes:                    # Share host directories with the container
    +  docker_mount_cwd_to_workspace: false  # SECURITY: off by default. Opt in to mount the launch cwd into /workspace.
    +  docker_volumes:                    # Additional explicit host mounts
         - "/home/user/projects:/workspace/projects"
         - "/home/user/data:/data:ro"     # :ro for read-only
     
    ```

    ```diff
    @@ -520,6 +521,31 @@ This is useful for:
     
     Can also be set via environment variable: `TERMINAL_DOCKER_VOLUMES='["/host:/container"]'` (JSON array).
     
    +### Optional: Mount the Launch Directory into `/workspace`
    +
    +Docker sandboxes stay isolated by default. Hermes does **not** pass your current host working directory into the container unless you explicitly opt in.
    +
    +Enable it in `config.yaml`:
    +
    +```yaml
    +terminal:
    +  backend: docker
    +  docker_mount_cwd_to_workspace: true
    +```
    +
    +When enabled:
    +- if you launch Hermes from `~/projects/my-app`, that host directory is bind-mounted to `/workspace`
    +- the Docker backend starts in `/workspace`
    +- file tools and terminal commands both see the same mounted project
    +
    +When disabled, `/workspace` stays sandbox-owned unless you explicitly mount something via `docker_volumes`.
    +
    +Security tradeoff:
    +- `false` preserves the sandbox boundary
    +- `true` gives the sandbox direct access to the directory you launched Hermes from
    +
    +Use the opt-in only when you intentionally want the container to work on live host files.
    +
     ### Persistent Shell
     
     By default, each terminal command runs in its own subprocess — working directory, environment variables, and shell variables reset between commands. When **persistent shell** is enabled, a single long-lived bash process is kept alive across `execute()` calls so that state survives between commands.
    ```
- **#1479: fix(docker): map current cwd into /workspace**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#1534`
- **#1504: fix(docker): auto-mount host CWD to /workspace**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#1534`
- **#1515: fix: mount host cwd to /workspace in Docker backend**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#1534`

## Fix status

- Merged to main in PR #1534 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-16`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `environments`
