fix(docker): auto-mount host CWD to /workspace

Fixes #1445 — When using Docker backend, the user's current working
directory is now automatically bind-mounted to /workspace inside the
container. This allows users to run `cd my-project && hermes` and have
their project files accessible to the agent without manual volume config.

Changes:
- Add host_cwd and auto_mount_cwd parameters to DockerEnvironment
- Capture original host CWD in _get_env_config() before container fallback
- Pass host_cwd through _create_environment() to Docker backend
- Add TERMINAL_DOCKER_NO_AUTO_MOUNT env var to disable if needed
- Skip auto-mount when /workspace is already explicitly mounted
- Add tests for auto-mount behavior
- Add documentation for the new feature

The auto-mount is skipped when:
1. TERMINAL_DOCKER_NO_AUTO_MOUNT=true is set
2. User configured docker_volumes with :/workspace
3. persistent_filesystem=true (persistent sandbox mode)

This makes the Docker backend behave more intuitively — the agent
operates on the user's actual project directory by default.
This commit is contained in:
Bartok9 2026-03-16 03:35:35 -04:00 committed by teknium1
parent ce660a4413
commit 8cdbbcaaa2
4 changed files with 222 additions and 2 deletions

View file

@ -158,6 +158,10 @@ class DockerEnvironment(BaseEnvironment):
Persistence: when enabled, bind mounts preserve /workspace and /root
across container restarts.
Auto-mount: when host_cwd is provided (the user's original working directory),
it is automatically bind-mounted to /workspace unless auto_mount_cwd=False
or the path is already covered by an explicit volume mount.
"""
def __init__(
@ -172,6 +176,8 @@ class DockerEnvironment(BaseEnvironment):
task_id: str = "default",
volumes: list = None,
network: bool = True,
host_cwd: str = None,
auto_mount_cwd: bool = True,
):
if cwd == "~":
cwd = "/root"
@ -250,6 +256,29 @@ class DockerEnvironment(BaseEnvironment):
else:
logger.warning(f"Docker volume '{vol}' missing colon, skipping")
# Auto-mount host CWD to /workspace when enabled (fixes #1445).
# This allows users to run `cd my-project && hermes` and have Docker
# automatically mount their project directory into the container.
# Disabled when: auto_mount_cwd=False, host_cwd is not a valid directory,
# or /workspace is already covered by writable_args or a user volume.
auto_mount_disabled = os.getenv("TERMINAL_DOCKER_NO_AUTO_MOUNT", "").lower() in ("1", "true", "yes")
if host_cwd and auto_mount_cwd and not auto_mount_disabled:
host_cwd_abs = os.path.abspath(os.path.expanduser(host_cwd))
if os.path.isdir(host_cwd_abs):
# Check if /workspace is already being mounted by persistence or user config
workspace_already_mounted = any(
":/workspace" in arg for arg in writable_args
) or any(
":/workspace" in arg for arg in volume_args
)
if not workspace_already_mounted:
logger.info(f"Auto-mounting host CWD to /workspace: {host_cwd_abs}")
volume_args.extend(["-v", f"{host_cwd_abs}:/workspace"])
else:
logger.debug(f"Skipping auto-mount: /workspace already mounted")
else:
logger.debug(f"Skipping auto-mount: host_cwd is not a valid directory: {host_cwd}")
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}")

View file

@ -481,7 +481,12 @@ def _get_env_config() -> Dict[str, Any]:
# 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.
cwd = os.getenv("TERMINAL_CWD", default_cwd)
raw_cwd = os.getenv("TERMINAL_CWD", default_cwd)
cwd = raw_cwd
# Capture original host CWD for auto-mounting into containers (fixes #1445).
# Even when the container's working directory falls back to /root, we still
# want to auto-mount the user's host project directory to /workspace.
host_cwd = raw_cwd if raw_cwd and os.path.isdir(raw_cwd) else os.getcwd()
if env_type in ("modal", "docker", "singularity", "daytona") and cwd:
# Host paths that won't exist inside containers
host_prefixes = ("/Users/", "/home/", "C:\\", "C:/")
@ -498,6 +503,7 @@ 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, # Original host directory for auto-mounting into containers
"timeout": _parse_env_var("TERMINAL_TIMEOUT", "180"),
"lifetime_seconds": _parse_env_var("TERMINAL_LIFETIME_SECONDS", "300"),
# SSH-specific config
@ -525,7 +531,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.
@ -537,6 +544,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: Original host working directory (for auto-mounting into containers)
Returns:
Environment instance with execute() method
@ -559,6 +567,7 @@ 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,
)
elif env_type == "singularity":
@ -965,6 +974,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({