{"repo":"NousResearch/hermes-agent","public_id":"1445","display_label":"Docker backend CWD-to-workspace mounting behavior","summary":"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.","subsystem":"environments","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":1534},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-03-16T12:42:21+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":1445,"pr_number":1534,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":1445,"title":"[Bug]: Hermes does not map current directory into docker backend automatically.","url":null,"state":"closed"},{"kind":"pr","number":1479,"title":"fix(docker): map current cwd into /workspace","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1534","number":1534}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":1504,"title":"fix(docker): auto-mount host CWD to /workspace","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1534","number":1534}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":1515,"title":"fix: mount host cwd to /workspace in Docker backend","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1534","number":1534}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":1534,"title":"fix(docker): make cwd workspace mount explicit opt-in","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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":[{"file":"cli-config.yaml.example","hunks":["@@ -76,8 +76,9 @@ model:\n #   - Messaging (Telegram/Discord): Uses MESSAGING_CWD from .env (default: home)\n terminal:\n   backend: \"local\"\n-  cwd: \".\"  # For local backend: \".\" = current directory. Ignored for remote backends.\n+  cwd: \".\"  # For local backend: \".\" = current directory. Ignored for remote backends unless a backend documents otherwise.\n   timeout: 180\n+  docker_mount_cwd_to_workspace: false  # SECURITY: off by default. Opt in to mount the launch cwd into Docker /workspace.\n   lifetime_seconds: 300\n   # sudo_password: \"\"  # Enable sudo commands (pipes via sudo -S) - SECURITY WARNING: plaintext!\n ","@@ -107,6 +108,7 @@ terminal:\n #   timeout: 180\n #   lifetime_seconds: 300\n #   docker_image: \"nikolaik/python-nodejs:python3.11-nodejs20\"\n+#   docker_mount_cwd_to_workspace: true   # Explicit opt-in: mount your launch cwd into /workspace\n \n # -----------------------------------------------------------------------------\n # OPTION 4: Singularity/Apptainer container"]},{"file":"cli.py","hunks":["@@ -165,6 +165,7 @@ def load_cli_config() -> Dict[str, Any]:\n             \"modal_image\": \"python:3.11\",\n             \"daytona_image\": \"nikolaik/python-nodejs:python3.11-nodejs20\",\n             \"docker_volumes\": [],  # host:container volume mounts for Docker backend\n+            \"docker_mount_cwd_to_workspace\": False,  # explicit opt-in only; default off for sandbox isolation\n         },\n         \"browser\": {\n             \"inactivity_timeout\": 120,  # Auto-cleanup inactive browser sessions after 2 min","@@ -330,6 +331,7 @@ def load_cli_config() -> Dict[str, Any]:\n         \"container_disk\": \"TERMINAL_CONTAINER_DISK\",\n         \"container_persistent\": \"TERMINAL_CONTAINER_PERSISTENT\",\n         \"docker_volumes\": \"TERMINAL_DOCKER_VOLUMES\",\n+        \"docker_mount_cwd_to_workspace\": \"TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE\",\n         \"sandbox_dir\": \"TERMINAL_SANDBOX_DIR\",\n         # Persistent shell (non-local backends)\n         \"persistent_shell\": \"TERMINAL_PERSISTENT_SHELL\","]},{"file":"hermes_cli/config.py","hunks":["@@ -118,6 +118,9 @@ def ensure_hermes_home():\n         # Each entry is \"host_path:container_path\" (standard Docker -v syntax).\n         # Example: [\"/home/user/projects:/workspace/projects\", \"/data:/data\"]\n         \"docker_volumes\": [],\n+        # Explicit opt-in: mount the host cwd into /workspace for Docker sessions.\n+        # Default off because passing host directories into a sandbox weakens isolation.\n+        \"docker_mount_cwd_to_workspace\": False,\n         # Persistent shell — keep a long-lived bash shell across execute() calls\n         # so cwd/env vars/shell variables survive between commands.\n         # Enabled by default for non-local backends (SSH); local is always opt-in","@@ -1407,6 +1410,7 @@ def set_config_value(key: str, value: str):\n         \"terminal.singularity_image\": \"TERMINAL_SINGULARITY_IMAGE\",\n         \"terminal.modal_image\": \"TERMINAL_MODAL_IMAGE\",\n         \"terminal.daytona_image\": \"TERMINAL_DAYTONA_IMAGE\",\n+        \"terminal.docker_mount_cwd_to_workspace\": \"TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE\",\n         \"terminal.cwd\": \"TERMINAL_CWD\",\n         \"terminal.timeout\": \"TERMINAL_TIMEOUT\",\n         \"terminal.sandbox_dir\": \"TERMINAL_SANDBOX_DIR\","]},{"file":"tests/hermes_cli/test_set_config_value.py","hunks":["@@ -115,3 +115,13 @@ def test_terminal_image_goes_to_config(self, _isolated_hermes_home):\n         set_config_value(\"terminal.docker_image\", \"python:3.12\")\n         config = _read_config(_isolated_hermes_home)\n         assert \"python:3.12\" in config\n+\n+    def test_terminal_docker_cwd_mount_flag_goes_to_config_and_env(self, _isolated_hermes_home):\n+        set_config_value(\"terminal.docker_mount_cwd_to_workspace\", \"true\")\n+        config = _read_config(_isolated_hermes_home)\n+        env_content = _read_env(_isolated_hermes_home)\n+        assert \"docker_mount_cwd_to_workspace: 'true'\" in config or \"docker_mount_cwd_to_workspace: true\" in config\n+        assert (\n+            \"TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE=true\" in env_content\n+            or \"TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE=True\" in env_content\n+        )"]},{"file":"tests/tools/test_docker_environment.py","hunks":["@@ -1,11 +1,31 @@\n import logging\n import subprocess\n+import sys\n+import types\n \n import pytest\n \n from tools.environments import docker as docker_env\n \n \n+def _install_fake_minisweagent(monkeypatch, captured_run_args):\n+    class MockInnerDocker:\n+        container_id = \"fake-container\"\n+        config = type(\"Config\", (), {\"executable\": \"/usr/bin/docker\", \"forward_env\": [], \"env\": {}})()\n+\n+        def __init__(self, **kwargs):\n+            captured_run_args.extend(kwargs.get(\"run_args\", []))\n+\n+    minisweagent_mod = types.ModuleType(\"minisweagent\")\n+    environments_mod = types.ModuleType(\"minisweagent.environments\")\n+    docker_mod = types.ModuleType(\"minisweagent.environments.docker\")\n+    docker_mod.DockerEnvironment = MockInnerDocker\n+\n+    monkeypatch.setitem(sys.modules, \"minisweagent\", minisweagent_mod)\n+    monkeypatch.setitem(sys.modules, \"minisweagent.environments\", environments_mod)\n+    monkeypatch.setitem(sys.modules, \"minisweagent.environments.docker\", docker_mod)\n+\n+\n def _make_dummy_env(**kwargs):\n     \"\"\"Helper to construct DockerEnvironment with minimal required args.\"\"\"\n     return docker_env.DockerEnvironment(","@@ -19,6 +39,8 @@ def _make_dummy_env(**kwargs):\n         task_id=kwargs.get(\"task_id\", \"test-task\"),\n         volumes=kwargs.get(\"volumes\", []),\n         network=kwargs.get(\"network\", True),\n+        host_cwd=kwargs.get(\"host_cwd\"),\n+        auto_mount_cwd=kwargs.get(\"auto_mount_cwd\", False),\n     )\n \n ","@@ -86,3 +108,106 @@ def _run(cmd, **kwargs):\n         })\n     ]\n \n+\n+def test_auto_mount_host_cwd_adds_volume(monkeypatch, tmp_path):\n+    \"\"\"Opt-in docker cwd mounting should bind the host cwd to /workspace.\"\"\"\n+    project_dir = tmp_path / \"my-project\"\n+    project_dir.mkdir()\n+\n+    def _run_docker_version(*args, **kwargs):\n+        return subprocess.CompletedProcess(args[0], 0, stdout=\"Docker version\", stderr=\"\")\n+\n+    monkeypatch.setattr(docker_env, \"find_docker\", lambda: \"/usr/bin/docker\")\n+    monkeypatch.setattr(docker_env.subprocess, \"run\", _run_docker_version)\n+\n+    captured_run_args = []\n+    _install_fake_minisweagent(monkeypatch, captured_run_args)\n+\n+    _make_dummy_env(\n+        cwd=\"/workspace\",\n+        host_cwd=str(project_dir),\n+        auto_mount_cwd=True,\n+    )\n+\n+    run_args_str = \" \".join(captured_run_args)\n+    assert f\"{project_dir}:/workspace\" in run_args_str\n+\n+\n+def test_auto_mount_disabled_by_default(monkeypatch, tmp_path):\n+    \"\"\"Host cwd should not be mounted unless the caller explicitly opts in.\"\"\"\n+    project_dir = tmp_path / \"my-project\"\n+    project_dir.mkdir()\n+\n+    def _run_docker_version(*args, **kwargs):\n+        return subprocess.CompletedProcess(args[0], 0, stdout=\"Docker version\", stderr=\"\")\n+\n+    monkeypatch.setattr(docker_env, \"find_docker\", lambda: \"/usr/bin/docker\")\n+    monkeypatch.setattr(docker_env.subprocess, \"run\", _run_docker_version)\n+\n+    captured_run_args = []\n+    _install_fake_minisweagent(monkeypatch, captured_run_args)\n+\n+    _make_dummy_env(\n+        cwd=\"/root\",\n+        host_cwd=str(project_dir),\n+        auto_mount_cwd=False,\n+    )\n+\n+    run_args_str = \" \".join(captured_run_args)\n+    assert f\"{project_dir}:/workspace\" not in run_args_str\n+\n+\n+def test_auto_mount_skipped_when_workspace_already_mounted(monkeypatch, tmp_path):\n+    \"\"\"Explicit user volumes for /workspace should take precedence over cwd mount.\"\"\"\n+    project_dir = tmp_path / \"my-project\"\n+    project_dir.mkdir()\n+    other_dir = tmp_path / \"other\"\n+    other_dir.mkdir()\n+\n+    def _run_docker_version(*args, **kwargs):\n+        return subprocess.CompletedProcess(args[0], 0, stdout=\"Docker version\", stderr=\"\")\n+\n+    monkeypatch.setattr(docker_env, \"find_docker\", lambda: \"/usr/bin/docker\")\n+    monkeypatch.setattr(docker_env.subprocess, \"run\", _run_docker_version)\n+\n+    captured_run_args = []\n+    _install_fake_minisweagent(monkeypatch, captured_run_args)\n+\n+    _make_dummy_env(\n+        cwd=\"/workspace\",\n+        host_cwd=str(project_dir),\n+        auto_mount_cwd=True,\n+        volumes=[f\"{other_dir}:/workspace\"],\n+    )\n+\n+    run_args_str = \" \".join(captured_run_args)\n+    assert f\"{other_dir}:/workspace\" in run_args_str\n+    assert run_args_str.count(\":/workspace\") == 1\n+\n+\n+def test_auto_mount_replaces_persistent_workspace_bind(monkeypatch, tmp_path):\n+    \"\"\"Persistent mode should still prefer the configured host cwd at /workspace.\"\"\"\n+    project_dir = tmp_path / \"my-project\"\n+    project_dir.mkdir()\n+\n+    def _run_docker_version(*args, **kwargs):\n+        return subprocess.CompletedProcess(args[0], 0, stdout=\"Docker version\", stderr=\"\")\n+\n+    monkeypatch.setattr(docker_env, \"find_docker\", lambda: \"/usr/bin/docker\")\n+    monkeypatch.setattr(docker_env.subprocess, \"run\", _run_docker_version)\n+\n+    captured_run_args = []\n+    _install_fake_minisweagent(monkeypatch, captured_run_args)\n+\n+    _make_dummy_env(\n+        cwd=\"/workspace\",\n+        persistent_filesystem=True,\n+        host_cwd=str(project_dir),\n+        auto_mount_cwd=True,\n+        task_id=\"test-persistent-auto-mount\",\n+    )\n+\n+    run_args_str = \" \".join(captured_run_args)\n+    assert f\"{project_dir}:/workspace\" in run_args_str\n+    assert \"/sandboxes/docker/test-persistent-auto-mount/workspace:/workspace\" not in run_args_str\n+"]},{"file":"tests/tools/test_modal_sandbox_fixes.py","hunks":["@@ -91,8 +91,8 @@ def test_home_path_replaced_for_modal(self):\n                 \"/home/ paths should be replaced for modal backend.\"\n             )\n \n-    def test_users_path_replaced_for_docker(self):\n-        \"\"\"TERMINAL_CWD=/Users/... should be replaced with /root for docker.\"\"\"\n+    def test_users_path_replaced_for_docker_by_default(self):\n+        \"\"\"Docker should keep host paths out of the sandbox unless explicitly enabled.\"\"\"\n         with patch.dict(os.environ, {\n             \"TERMINAL_ENV\": \"docker\",\n             \"TERMINAL_CWD\": \"/Users/someone/projects\",","@@ -100,8 +100,22 @@ def test_users_path_replaced_for_docker(self):\n             config = _tt_mod._get_env_config()\n             assert config[\"cwd\"] == \"/root\", (\n                 f\"Expected /root, got {config['cwd']}. \"\n-                \"/Users/ paths should be replaced for docker backend.\"\n+                \"Host paths should be discarded for docker backend by default.\"\n             )\n+            assert config[\"host_cwd\"] is None\n+            assert config[\"docker_mount_cwd_to_workspace\"] is False\n+\n+    def test_users_path_maps_to_workspace_for_docker_when_enabled(self):\n+        \"\"\"Docker should map the host cwd into /workspace only when explicitly enabled.\"\"\"\n+        with patch.dict(os.environ, {\n+            \"TERMINAL_ENV\": \"docker\",\n+            \"TERMINAL_CWD\": \"/Users/someone/projects\",\n+            \"TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE\": \"true\",\n+        }):\n+            config = _tt_mod._get_env_config()\n+            assert config[\"cwd\"] == \"/workspace\"\n+            assert config[\"host_cwd\"] == \"/Users/someone/projects\"\n+            assert config[\"docker_mount_cwd_to_workspace\"] is True\n \n     def test_windows_path_replaced_for_modal(self):\n         \"\"\"TERMINAL_CWD=C:\\\\Users\\\\... should be replaced for modal.\"\"\"","@@ -119,12 +133,27 @@ def test_default_cwd_is_root_for_container_backends(self):\n                 # Remove TERMINAL_CWD so it uses default\n                 env = os.environ.copy()\n                 env.pop(\"TERMINAL_CWD\", None)\n+                env.pop(\"TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE\", None)\n                 with patch.dict(os.environ, env, clear=True):\n                     config = _tt_mod._get_env_config()\n                     assert config[\"cwd\"] == \"/root\", (\n                         f\"Backend {backend}: expected /root default, got {config['cwd']}\"\n                     )\n \n+    def test_docker_default_cwd_maps_current_directory_when_enabled(self):\n+        \"\"\"Docker should use /workspace when cwd mounting is explicitly enabled.\"\"\"\n+        with patch(\"tools.terminal_tool.os.getcwd\", return_value=\"/home/user/project\"):\n+            with patch.dict(os.environ, {\n+                \"TERMINAL_ENV\": \"docker\",\n+                \"TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE\": \"true\",\n+            }, clear=False):\n+                env = os.environ.copy()\n+                env.pop(\"TERMINAL_CWD\", None)\n+                with patch.dict(os.environ, env, clear=True):\n+                    config = _tt_mod._get_env_config()\n+                    assert config[\"cwd\"] == \"/workspace\"\n+                    assert config[\"host_cwd\"] == \"/home/user/project\"\n+\n     def test_local_backend_uses_getcwd(self):\n         \"\"\"Local backend should use os.getcwd(), not /root.\"\"\"\n         with patch.dict(os.environ, {\"TERMINAL_ENV\": \"local\"}, clear=False):","@@ -134,6 +163,31 @@ def test_local_backend_uses_getcwd(self):\n                 config = _tt_mod._get_env_config()\n                 assert config[\"cwd\"] == os.getcwd()\n \n+    def test_create_environment_passes_docker_host_cwd_and_flag(self, monkeypatch):\n+        \"\"\"Docker host cwd and mount flag should reach DockerEnvironment.\"\"\"\n+        captured = {}\n+        sentinel = object()\n+\n+        def _fake_docker_environment(**kwargs):\n+            captured.update(kwargs)\n+            return sentinel\n+\n+        monkeypatch.setattr(_tt_mod, \"_DockerEnvironment\", _fake_docker_environment)\n+\n+        env = _tt_mod._create_environment(\n+            env_type=\"docker\",\n+            image=\"python:3.11\",\n+            cwd=\"/workspace\",\n+            timeout=60,\n+            container_config={\"docker_mount_cwd_to_workspace\": True},\n+            host_cwd=\"/home/user/project\",\n+        )\n+\n+        assert env is sentinel\n+        assert captured[\"cwd\"] == \"/workspace\"\n+        assert captured[\"host_cwd\"] == \"/home/user/project\"\n+        assert captured[\"auto_mount_cwd\"] is True\n+\n     def test_ssh_preserves_home_paths(self):\n         \"\"\"SSH backend should NOT replace /home/ paths (they're valid remotely).\"\"\"\n         with patch.dict(os.environ, {"]},{"file":"tools/environments/docker.py","hunks":["@@ -172,6 +172,8 @@ def __init__(\n         task_id: str = \"default\",\n         volumes: list = None,\n         network: bool = True,\n+        host_cwd: str = None,\n+        auto_mount_cwd: bool = False,\n     ):\n         if cwd == \"~\":\n             cwd = \"/root\"","@@ -214,30 +216,9 @@ def __init__(\n         # mode uses tmpfs (ephemeral, fast, gone on cleanup).\n         from tools.environments.base import get_sandbox_dir\n \n-        self._workspace_dir: Optional[str] = None\n-        self._home_dir: Optional[str] = None\n-        if self._persistent:\n-            sandbox = get_sandbox_dir() / \"docker\" / task_id\n-            self._workspace_dir = str(sandbox / \"workspace\")\n-            self._home_dir = str(sandbox / \"home\")\n-            os.makedirs(self._workspace_dir, exist_ok=True)\n-            os.makedirs(self._home_dir, exist_ok=True)\n-            writable_args = [\n-                \"-v\", f\"{self._workspace_dir}:/workspace\",\n-                \"-v\", f\"{self._home_dir}:/root\",\n-            ]\n-        else:\n-            writable_args = [\n-                \"--tmpfs\", \"/workspace:rw,exec,size=10g\",\n-                \"--tmpfs\", \"/home:rw,exec,size=1g\",\n-                \"--tmpfs\", \"/root:rw,exec,size=1g\",\n-            ]\n-\n-        # All containers get security hardening (capabilities dropped, no privilege\n-        # escalation, PID limits). The container filesystem is writable so agents\n-        # can install packages as needed.\n         # User-configured volume mounts (from config.yaml docker_volumes)\n         volume_args = []\n+        workspace_explicitly_mounted = False\n         for vol in (volumes or []):\n             if not isinstance(vol, str):\n                 logger.warning(f\"Docker volume entry is not a string: {vol!r}\")","@@ -247,9 +228,53 @@ def __init__(\n                 continue\n             if \":\" in vol:\n                 volume_args.extend([\"-v\", vol])\n+                if \":/workspace\" in vol:\n+                    workspace_explicitly_mounted = True\n             else:\n                 logger.warning(f\"Docker volume '{vol}' missing colon, skipping\")\n \n+        host_cwd_abs = os.path.abspath(os.path.expanduser(host_cwd)) if host_cwd else \"\"\n+        bind_host_cwd = (\n+            auto_mount_cwd\n+            and bool(host_cwd_abs)\n+            and os.path.isdir(host_cwd_abs)\n+            and not workspace_explicitly_mounted\n+        )\n+        if auto_mount_cwd and host_cwd and not os.path.isdir(host_cwd_abs):\n+            logger.debug(f\"Skipping docker cwd mount: host_cwd is not a valid directory: {host_cwd}\")\n+\n+        self._workspace_dir: Optional[str] = None\n+        self._home_dir: Optional[str] = None\n+        writable_args = []\n+        if self._persistent:\n+            sandbox = get_sandbox_dir() / \"docker\" / task_id\n+            self._home_dir = str(sandbox / \"home\")\n+            os.makedirs(self._home_dir, exist_ok=True)\n+            writable_args.extend([\n+                \"-v\", f\"{self._home_dir}:/root\",\n+            ])\n+            if not bind_host_cwd and not workspace_explicitly_mounted:\n+                self._workspace_dir = str(sandbox / \"workspace\")\n+                os.makedirs(self._workspace_dir, exist_ok=True)\n+                writable_args.extend([\n+                    \"-v\", f\"{self._workspace_dir}:/workspace\",\n+                ])\n+        else:\n+            if not bind_host_cwd and not workspace_explicitly_mounted:\n+                writable_args.extend([\n+                    \"--tmpfs\", \"/workspace:rw,exec,size=10g\",\n+                ])\n+            writable_args.extend([\n+                \"--tmpfs\", \"/home:rw,exec,size=1g\",\n+                \"--tmpfs\", \"/root:rw,exec,size=1g\",\n+            ])\n+\n+        if bind_host_cwd:\n+            logger.info(f\"Mounting configured host cwd to /workspace: {host_cwd_abs}\")\n+            volume_args = [\"-v\", f\"{host_cwd_abs}:/workspace\", *volume_args]\n+        elif workspace_explicitly_mounted:\n+            logger.debug(\"Skipping docker cwd mount: /workspace already mounted by user config\")\n+\n         logger.info(f\"Docker volume_args: {volume_args}\")\n         all_run_args = list(_SECURITY_ARGS) + writable_args + resource_args + volume_args\n         logger.info(f\"Docker run_args: {all_run_args}\")"]},{"file":"tools/file_tools.py","hunks":["@@ -140,6 +140,7 @@ def _get_file_ops(task_id: str = \"default\") -> ShellFileOperations:\n                 container_config=container_config,\n                 local_config=local_config,\n                 task_id=task_id,\n+                host_cwd=config.get(\"host_cwd\"),\n             )\n \n             with _env_lock:"]},{"file":"tools/terminal_tool.py","hunks":["@@ -466,6 +466,8 @@ def _get_env_config() -> Dict[str, Any]:\n     default_image = \"nikolaik/python-nodejs:python3.11-nodejs20\"\n     env_type = os.getenv(\"TERMINAL_ENV\", \"local\")\n     \n+    mount_docker_cwd = os.getenv(\"TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE\", \"false\").lower() in (\"true\", \"1\", \"yes\")\n+\n     # Default cwd: local uses the host's current directory, everything\n     # else starts in the user's home (~ resolves to whatever account\n     # is running inside the container/remote).","@@ -475,16 +477,25 @@ def _get_env_config() -> Dict[str, Any]:\n         default_cwd = \"~\"\n     else:\n         default_cwd = \"/root\"\n-    \n+\n     # Read TERMINAL_CWD but sanity-check it for container backends.\n-    # If the CWD looks like a host-local path that can't exist inside a\n-    # container/sandbox, fall back to the backend's own default. This\n-    # catches the case where cli.py (or .env) leaked the host's CWD.\n-    # SSH is excluded since /home/ paths are valid on remote machines.\n+    # If Docker cwd passthrough is explicitly enabled, remap the host path to\n+    # /workspace and track the original host path separately. Otherwise keep the\n+    # normal sandbox behavior and discard host paths.\n     cwd = os.getenv(\"TERMINAL_CWD\", default_cwd)\n-    if env_type in (\"modal\", \"docker\", \"singularity\", \"daytona\") and cwd:\n+    host_cwd = None\n+    host_prefixes = (\"/Users/\", \"/home/\", \"C:\\\\\", \"C:/\")\n+    if env_type == \"docker\" and mount_docker_cwd:\n+        docker_cwd_source = os.getenv(\"TERMINAL_CWD\") or os.getcwd()\n+        candidate = os.path.abspath(os.path.expanduser(docker_cwd_source))\n+        if (\n+            any(candidate.startswith(p) for p in host_prefixes)\n+            or (os.path.isabs(candidate) and os.path.isdir(candidate) and not candidate.startswith((\"/workspace\", \"/root\")))\n+        ):\n+            host_cwd = candidate\n+            cwd = \"/workspace\"\n+    elif env_type in (\"modal\", \"docker\", \"singularity\", \"daytona\") and cwd:\n         # Host paths that won't exist inside containers\n-        host_prefixes = (\"/Users/\", \"/home/\", \"C:\\\\\", \"C:/\")\n         if any(cwd.startswith(p) for p in host_prefixes) and cwd != default_cwd:\n             logger.info(\"Ignoring TERMINAL_CWD=%r for %s backend \"\n                         \"(host path won't exist in sandbox). Using %r instead.\",","@@ -498,6 +509,8 @@ def _get_env_config() -> Dict[str, Any]:\n         \"modal_image\": os.getenv(\"TERMINAL_MODAL_IMAGE\", default_image),\n         \"daytona_image\": os.getenv(\"TERMINAL_DAYTONA_IMAGE\", default_image),\n         \"cwd\": cwd,\n+        \"host_cwd\": host_cwd,\n+        \"docker_mount_cwd_to_workspace\": mount_docker_cwd,\n         \"timeout\": _parse_env_var(\"TERMINAL_TIMEOUT\", \"180\"),\n         \"lifetime_seconds\": _parse_env_var(\"TERMINAL_LIFETIME_SECONDS\", \"300\"),\n         # SSH-specific config","@@ -525,7 +538,8 @@ def _get_env_config() -> Dict[str, Any]:\n def _create_environment(env_type: str, image: str, cwd: str, timeout: int,\n                         ssh_config: dict = None, container_config: dict = None,\n                         local_config: dict = None,\n-                        task_id: str = \"default\"):\n+                        task_id: str = \"default\",\n+                        host_cwd: str = None):\n     \"\"\"\n     Create an execution environment from mini-swe-agent.\n     ","@@ -537,6 +551,7 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,\n         ssh_config: SSH connection config (for env_type=\"ssh\")\n         container_config: Resource config for container backends (cpu, memory, disk, persistent)\n         task_id: Task identifier for environment reuse and snapshot keying\n+        host_cwd: Optional host working directory to bind into Docker when explicitly enabled\n         \n     Returns:\n         Environment instance with execute() method","@@ -559,6 +574,8 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,\n             cpu=cpu, memory=memory, disk=disk,\n             persistent_filesystem=persistent, task_id=task_id,\n             volumes=volumes,\n+            host_cwd=host_cwd,\n+            auto_mount_cwd=cc.get(\"docker_mount_cwd_to_workspace\", False),\n         )\n     \n     elif env_type == \"singularity\":","@@ -948,6 +965,7 @@ def terminal_tool(\n                                 \"container_disk\": config.get(\"container_disk\", 51200),\n                                 \"container_persistent\": config.get(\"container_persistent\", True),\n                                 \"docker_volumes\": config.get(\"docker_volumes\", []),\n+                                \"docker_mount_cwd_to_workspace\": config.get(\"docker_mount_cwd_to_workspace\", False),\n                             }\n \n                         local_config = None","@@ -965,6 +983,7 @@ def terminal_tool(\n                             container_config=container_config,\n                             local_config=local_config,\n                             task_id=effective_task_id,\n+                            host_cwd=config.get(\"host_cwd\"),\n                         )\n                     except ImportError as e:\n                         return json.dumps({"]},{"file":"website/docs/reference/environment-variables.md","hunks":["@@ -79,6 +79,7 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe\n | `TERMINAL_ENV` | Backend: `local`, `docker`, `ssh`, `singularity`, `modal`, `daytona` |\n | `TERMINAL_DOCKER_IMAGE` | Docker image (default: `python:3.11`) |\n | `TERMINAL_DOCKER_VOLUMES` | Additional Docker volume mounts (comma-separated `host:container` pairs) |\n+| `TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE` | Advanced opt-in: mount the launch cwd into Docker `/workspace` (`true`/`false`, default: `false`) |\n | `TERMINAL_SINGULARITY_IMAGE` | Singularity image or `.sif` path |\n | `TERMINAL_MODAL_IMAGE` | Modal container image |\n | `TERMINAL_DAYTONA_IMAGE` | Daytona sandbox image |"]},{"file":"website/docs/user-guide/configuration.md","hunks":["@@ -453,7 +453,8 @@ terminal:\n \n   # Docker-specific settings\n   docker_image: \"nikolaik/python-nodejs:python3.11-nodejs20\"\n-  docker_volumes:                    # Share host directories with the container\n+  docker_mount_cwd_to_workspace: false  # SECURITY: off by default. Opt in to mount the launch cwd into /workspace.\n+  docker_volumes:                    # Additional explicit host mounts\n     - \"/home/user/projects:/workspace/projects\"\n     - \"/home/user/data:/data:ro\"     # :ro for read-only\n ","@@ -520,6 +521,31 @@ This is useful for:\n \n Can also be set via environment variable: `TERMINAL_DOCKER_VOLUMES='[\"/host:/container\"]'` (JSON array).\n \n+### Optional: Mount the Launch Directory into `/workspace`\n+\n+Docker sandboxes stay isolated by default. Hermes does **not** pass your current host working directory into the container unless you explicitly opt in.\n+\n+Enable it in `config.yaml`:\n+\n+```yaml\n+terminal:\n+  backend: docker\n+  docker_mount_cwd_to_workspace: true\n+```\n+\n+When enabled:\n+- if you launch Hermes from `~/projects/my-app`, that host directory is bind-mounted to `/workspace`\n+- the Docker backend starts in `/workspace`\n+- file tools and terminal commands both see the same mounted project\n+\n+When disabled, `/workspace` stays sandbox-owned unless you explicitly mount something via `docker_volumes`.\n+\n+Security tradeoff:\n+- `false` preserves the sandbox boundary\n+- `true` gives the sandbox direct access to the directory you launched Hermes from\n+\n+Use the opt-in only when you intentionally want the container to work on live host files.\n+\n ### Persistent Shell\n \n 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.\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}