Status: Accepted (2026-05-07).
ADR 0007 shipped eden.interactive() for no_sandbox only. Other providers raise InvalidOptions. Users running interactive Claude Code sessions inside docker / podman containers need a parallel path.
The blocker is that the SandboxHandle Protocol only exposes exec() for non-interactive command invocations. Interactive sessions need:
- a TTY allocated inside the container (
docker exec -it ...); - the parent's stdin / stdout / stderr inherited (no
subprocess.PIPEcapturing); - the container's working directory and any per-call env vars threaded;
- correct teardown when the user exits the agent (the container persists, only the
execexits).
Three options were considered:
- Special-case docker / podman in
_interactive.py— branch onprovider.name == "docker"and inline thedocker exec -itinvocation. Quick but couples the orchestrator to a binary name and breaks for any future container provider. - Add
interactive_exec()to the bind-mount handle Protocol. Each container provider implements it with its own binary. The orchestrator's_interactive.pycheckshasattr(handle, "interactive_exec")and dispatches; falls back to directsubprocess.runforno_sandbox. Clean Protocol extension, lets the isolated provider opt out (its handle has nothing to attach to). - Build it as a top-level helper that takes a
SandboxHandleand an argv —eden.exec_interactive(handle, argv, ...)— and call it from_interactive.py. Pushes the same logic into a public surface that's mostly redundant withinteractive().
Adopt option 2.
- Extend
BindMountSandboxHandle(the marker Protocol covering docker, podman, no_sandbox) with an optionalinteractive_exec(argv, *, cwd=None, env=None) -> intmethod that runs the argv with stdio inherited and returns the process exit code. no_sandbox._NoSandboxHandle.interactive_execrunssubprocess.run(argv, cwd=cwd, env=env)directly — the existing_interactive.pyflow extracted into a method._ContainerHandle.interactive_execbuilds[binary, "exec", "-it", "-w", cwd_in_container, "-e", k=v..., container_id, *argv]and runs it with stdio inherited. The-itflag allocates a pseudo-TTY and keeps stdin attached._interactive.pydrops theprovider.name == "no_sandbox"gate and instead checkshasattr(handle, "interactive_exec"). For containers, the agent argv is built once (viabuild_interactive_command) and passed through; the worktree path is translated to the in-container path (/workspace).- Isolated providers (Daytona, Vercel, our local isolated copy) don't implement
interactive_exec. Callinginteractive()against them keeps the originalInvalidOptionserror with an updated hint (something like "isolated providers don't expose a TTY; use docker / podman / no_sandbox").
The subprocess.run in _interactive.py for no_sandbox becomes a single-line wrapper around handle.interactive_exec(...). Hooks (OnSandboxReady, OnClose) already work for any handle; no changes needed there.
eden.interactive(sandbox=docker_provider(...))becomes a real workflow: drop into a Claude TUI inside a clean worktree inside a container that has the project's deps installed.- The Protocol extension is opt-in. Existing custom providers that don't implement
interactive_execbehave exactly as today (raiseInvalidOptionsfrom the orchestrator with a clear hint). The runtime check ishasattr, notisinstance. - Container env propagation matches
exec()'s shape: each-e KEY=VALUEis forwarded. Hooks that prepared the container (e.g.npm install) still run beforeinteractive_exec. - Per-call cwd is translated host → container by the orchestrator before invocation. The handle's
worktree_path(which equals/workspacefor docker / podman) is the natural default. - Docker-on-macOS and Docker-on-Windows both support
-it; Podman requires--userns=keep-idfor rootless TTY but eden already passes the user via--user uid:gidso this is unaffected.
- ADR 0007 — the no_sandbox-only baseline this builds on.
- Sandcastle 0.4.6 introduced
interactiveExecon its docker / podman providers with the same shape. eden/providers/_protocols.py—BindMountSandboxHandlemarker.eden/providers/_impl/container.py,eden/sandboxes/no_sandbox/__init__.py— implementation sites.