Skip to content

[Feature]: Container worktrees: run each worktree and its agents in an isolated Docker/Podman container (checkbox + default setting, works on Orca Server) #29578

Closed

Description

@yoobi

Problem or use case

Orca launches every agent with its permission-bypass flag by default (--dangerously-skip-permissions, --dangerously-bypass-approvals-and-sandbox, --yolo). The docs say "the worktree itself is the sandbox" (docs/site/content/docs/model/agents-sessions.mdx), but also: "A worktree is an isolated checkout, not a security sandbox: the agent can still access files and network resources available to its process" (docs/site/content/docs/agents/supported.mdx).

So one wrong command from one agent damages the whole machine. Examples: rm -rf ~, rm -rf /, sudo apt remove …, an edit to ~/.ssh or ~/.bashrc, reading ~/.aws, killing other processes. On a headless Orca Server shared by many workspaces, that also takes down every other workspace and the server itself.

The vendors of the agents Orca launches all say the same: bypass mode belongs inside an external boundary.

Orca currently provides no such boundary. The experimental Cloud VM recipes come closest, but they run only on the desktop ("Ephemeral VM recipes run on the local desktop host in v1", src/main/ipc/ephemeral-vm-recipe-context.ts). They need a hand-written sshd image per provider, and no recipe ships with Orca.

Summary: I want to tick "Run in an isolated container" when I create a worktree, on Desktop or on Orca Server, so that whatever the agent breaks stays inside that container.

Wording: below, "workspace" means an Orca workspace backed by a git worktree. Each worktree gets its own container, and two worktrees of the same repo never share one.

Proposed solution

0. Start from what already works: orca-docker

A community recipe, orca-docker (https://github.com/talayolabs/orca-docker), already runs one container per Orca worktree with nothing mounted from the host on today's recipe API (checkoutMode: provisioned-root):

  • the container gets its own clone at the same absolute path;
  • Orca's file tree, editor, git and terminals run inside it over SSH;
  • deleting the worktree destroys the container;
  • it comes with an end-to-end smoke test and CI.

It is the best starting point. Compared with this request (checked at commit 5331cdb):

Requirement orca-docker today
One container per worktree, nothing on the host, removed on delete ✅
Orca's tools (files, git, terminals, agents) inside the container ✅ (over SSH)
Forward Orca/agent variables ✅ partly: -e NAME (name only) for ORCA_*, CLAUDE_*, ANTHROPIC_*, GIT_AUTHOR_*…; no project or container-only layer (§4)
Checkbox + default setting ❌ chosen per worktree through "Run on"
Orca Server / CLI / mobile ❌ recipes run on the desktop only (#15587, #20613, #26879)
Hardening (--cap-drop ALL, no-new-privileges, limits) ❌ not set by default (ORCA_DOCKER_ARGS can add flags)
Credentials ⚠️ copies the host's ~/.claude (including .credentials.json), ~/.claude.json and ~/.gitconfig into the container, which Anthropic advises against (§3)
Agents Claude Code only, one agent per container
Overhead sshd, key and published port per container; full desktop image (XFCE, Chromium, noVNC), which is also a feature
License none published, so it can't be copied into Orca (MIT) without the author's agreement

Suggested path:

  • Phase 1 (quick): with the author's agreement, make orca-docker's model an official or bundled recipe/plugin, adding the §3 hardening defaults and replacing the credential copy with a per-worktree agent-config volume. Desktop users get container worktrees almost immediately.
  • Phase 2 (core): the DockerOrcadHostTransport described in §5. It removes the sshd/key/port per container and the desktop-only limit, and brings the checkbox, the default setting, CLI and Orca Server support, and the environment layers.

1. User experience

Where What
Settings → Experimental Container workspaces toggle (experimentalContainerWorkspaces, off by default), next to experimentalEphemeralVms (src/shared/default-global-settings.ts, src/renderer/src/components/settings/EphemeralVmsExperimentalSetting.tsx).
Settings (when the flag is on) Run new workspaces in a container by default: Off / On. Also: default mode (Container only / Shared folder), engine (Docker / Podman, auto-detected), image, and a resource template (Small 2 GB·1 CPU / Medium 4 GB·2 CPU / Large 8 GB·4 CPU).
New-workspace dialog A Run in an isolated container checkbox, pre-filled from the default and overridable per workspace, with mode and template under "Advanced". It goes next to the other SettingsSwitch rows in NewWorkspaceComposerAdvancedSection.tsx, or under "Run on".
Add project → Clone repository Clone into a container option: the project lives only in a container volume from the start.
CLI orca worktree create --container[=volume|mounted] [--container-image <ref>] [--container-template small|medium|large] and --no-container. Flags go in src/cli/specs/core.ts; params extend the WorktreeCreate zod schema in src/shared/rpc-contract/worktree-create-params.ts. CLI, mobile and Orca Server then all get it (cf. #15587, #20613, #17292).
Per repo An orca.yaml block (below). Precedence: dialog > repo settings > orca.yaml > global default.
Environment variables The project's variables are forwarded into the container. Container-only variables can be added per project or per workspace: in the dialog, in the workspace's container panel after creation, or with --container-env KEY=VALUE / --container-env-file <path>. See §4.
Sidebar Container badge on the workspace, container state (running / stopped / missing), and Restart / Recreate actions.
Delete workspace Removes the container, and in volume mode the volume too. Nothing is left behind.
worktree:
  container:
    mode: volume                 # volume (container only) | mounted (shared folder)
    image: ghcr.io/acme/agent-base:1   # or "devcontainer" to build .devcontainer/devcontainer.json
    template: medium             # small | medium | large
    network: bridge              # bridge | none | allowlist (later)
    env:                         # container-only variables (non-secret; secrets go in repo settings / secret store)
      NODE_ENV: development
      DATABASE_URL: postgres://db:5432/app_dev   # differs from the host's value
    forwardHostEnv: [GRADLE_OPTS, NPM_CONFIG_REGISTRY]   # host variable names to forward, never values
    copyIgnoredFiles: true       # volume mode: copy the .worktreeinclude files (.env, local.properties…) into the volume

2. Two storage modes

Container only (volume), recommended default Shared folder (mounted)
Where the code lives A git clone inside a named Docker volume (orca-ws-<id>); nothing on the host The host worktree, bind-mounted at the same absolute path
rm -rf ~ / rm -rf / by the agent Destroys only the container Destroys only the container, except the mounted worktree
Agent damages the worktree itself Container copy only; pushed branches are safe Host worktree affected (committed work safe in git)
Delete workspace docker rm -f + docker volume rm: everything gone docker rm -f; the worktree is removed as today
How changes come out git push of the workspace branch (Orca's Source Control runs inside the container) Directly on the host
Precedent orca-docker, the same model on today's recipe API (https://github.com/talayolabs/orca-docker); VS Code "Clone Repository in Container Volume" (https://code.visualstudio.com/docs/devcontainers/containers); Docker Sandboxes clone mode (https://docs.docker.com/ai/sandboxes/security/) Codeman Docker cases (https://github.com/Ark0N/Codeman/blob/master/docs/docker-cases.md), Docker Sandboxes direct mode
Main risk none on the host Docker warns that "Git hooks, CI config, IDE task configs, Makefiles, and package.json scripts can run on your host… Git hooks sit in .git/ and don't appear in git diff" (https://docs.docker.com/ai/sandboxes/security/)

3. Security defaults (all from vendor or standards guidance)

4. Environment variables: forward the project's, add container-only ones

A container does not inherit the host's environment. A docker exec process sees only what was set when the container was created, plus what -e/--env-file passes to that one process (https://docs.docker.com/reference/cli/docker/container/exec/). Orca therefore has to forward variables explicitly. Proposed layers, lowest precedence first:

# Layer Where it lives in Orca today In the container
1 Image ENV, or devcontainer containerEnv as is
2 Orca's own ORCA_* (terminal handle, worktree id, agent hook port/token…) the explicit WSL passthrough list in addOrcaWslInteropEnv (src/main/pty/wsl-orca-env.ts, called from src/main/daemon/pty-subprocess/shell-launch-plan.ts), with path-valued entries translated always forwarded and reserved (cannot be overridden). In volume mode the in-container orcad sets them itself
3 Global agent env agentDefaultEnv (Settings → Agents; src/shared/global-settings-types.ts, resolved in src/shared/agent-startup-plan-inputs.ts) forwarded
4 Project env per-project agent env/args (#25289 proposes agent.env in orca.yaml + repo settings); per-project gh account, where Orca mints a short-lived token and "injects it into that child env only" (Repo.ghAccount, src/main/git/command-runner/gh-bound-account-env.ts) forwarded; the GitHub token goes in as GH_TOKEN
5 Project's gitignored env files (.env, .env.local, Android local.properties…) .worktreeinclude already copies the listed gitignored files into new worktrees (src/main/git/worktree-include-file.ts) mounted mode: already in the worktree. Volume mode: git clone doesn't bring them, so Orca copies the .worktreeinclude files from the host project into the volume after cloning (opt-in, copyIgnoredFiles, listed in the dialog)
6 Host shell variables opt-in allowlist of names, like native chat's nativeChatShellEnvironment (src/shared/native-chat-shell-environment.ts) only the listed names (forwardHostEnv). Never inherit everything, because the host env often holds cloud credentials
7 Secrets the project secret store proposed in #24182 (SecretStore, src/shared/secret-store.ts, keychain-sealed) injected by name when each process is launched
8 Container-only variables (new) none see below

Container-only variables exist only inside that container, never on the host:

  • Per project, for every container workspace of the repo: worktree.container.env in orca.yaml for non-secret values (committed). The same field in repo settings holds local or secret values (not committed), using the shared/local split commandSourcePolicy already uses for scripts.
  • Per workspace: an "Environment variables" editor in the dialog's container section and in the workspace's container panel after creation. CLI: --container-env KEY=VALUE (repeatable) and --container-env-file <path>.
  • Editing after creation needs no recreate. Variables are passed per docker exec, so a change applies to every new terminal or agent immediately, and running ones keep their old env (the UI should say so). Variables needed at container start (for the entrypoint) form a separate "requires recreate" list.

Rules:

5. Implementation: Orca already has the seam

Container only (volume) = a new orcad host transport.

  • SSH and WSL hosts already run orcad behind OrcadHostTransport (src/main/ssh/orcad-host-transport.ts, built on RemoteInstallTransport in src/main/ssh/remote-install-transport.ts). deployOrcad (src/main/ssh/orcad-remote-deploy.ts) accepts any transport.
  • WSL is the second implementation: createWslOrcadHostTransport (src/main/wsl/wsl-orcad-host-transport.ts) runs wsl.exe --exec sh -c, and src/main/wsl/wsl-orcad-deploy.ts reuses the SSH install/activation.
  • The stdio bridge is transport-agnostic (src/main/ssh/orcad-stdio-bridge-provider.ts).
  • So:
    1. Add DockerOrcadHostTransport (docker|podman exec -i <container> sh -c …), modelled on the WSL one.
    2. Add a docker target to OrcadHostTarget and a docker variant to OrcadDeploymentLinkSchema (src/shared/runtime-environments.ts, ssh|wsl today).
    3. Provision: docker volume create → docker run -d (flags from §3, volume at /workspace) → deployOrcad → register as a runtime environment → repo.clone into /workspace (src/main/runtime/rpc/methods/repo.ts) → create the worktree on that runtime.
  • Files, git, PTYs and agent hooks then work as on any runtime host. orcad runs its own hook server (src/main/orcad/orcad-managed-agent-hooks.ts), so the container never needs to reach the host.
  • Provisioning today lives in the renderer (src/renderer/src/lib/ephemeral-vm-worktree-creation.ts). It should live in main so the CLI, mobile and a headless Orca Server can trigger it.
  • Precedent for a server owning hosts: runtime-owned SSH targets (src/main/runtime/rpc/methods/ssh.ts).

Shared folder (mounted) = a spawn wrapper.

  • Wrap each workspace PTY in docker exec -it -w <path> -e … <container> inside the local PTY launch plan, where WSL is already special-cased (src/main/providers/local-pty-launch-plan.ts). Alternatively, rebase PR Add Project from a Devcontainer (docker exec, GUI source) #6426's DockerPtyProvider.
  • Agent hooks: the hook server listens on 127.0.0.1 only (ORCA_AGENT_HOOK_PORT / ORCA_AGENT_HOOK_TOKEN / ORCA_AGENT_HOOK_ENDPOINT). Either relay it like src/main/agent-hooks/wsl-hook-relay-manager.ts, or pass it through --add-host host.docker.internal:host-gateway with -e variables.

Shared by both modes:

  • Persist container: { engine, id, mode, image, volume? } in WorktreeMeta (src/shared/worktree/meta-types.ts), or in a side store like orca-ephemeral-vm-runtimes.json (src/shared/ephemeral-vm-runtime-store.ts) so orphan sweeps survive metadata garbage collection.
  • Cleanup belongs on the main side so CLI, mobile and server deletes all clean up: executeWorktreeRemoval (src/main/ipc/worktrees/removal/execute-worktree-removal.ts) and removeManagedWorktree (src/main/runtime/workspaces/workspace-removal.ts). Add a startup sweep of label=orca.workspace containers whose workspace no longer exists.
  • Engine access: reuse PR feat(docker): Docker Services panel (containers, logs, shell, lifecycle, images/volumes/networks, connections) #5522's docker-command-runner.ts (execFile, contexts, DOCKER_HOST=ssh://).
  • Image: orca.yaml image, or devcontainer.json (with the restrictions in §3), or an Orca base image containing sh, tar and git. orcad brings its own Node (src/main/ssh/orcad-remote-node-runtime.ts).

6. Suggested PR slices

  1. Flag + settings + schema: experimentalContainerWorkspaces, the global default, the worktree.container yaml parser with tests, WorktreeCreate.container, and docker/podman detection.
  2. DockerOrcadHostTransport + "Attach to an existing container" as a host, like WSL hosts. This is testable on its own.
  3. Container only per workspace: provisioning in main, the composer checkbox, CLI --container, cleanup on delete, and the orphan sweep.
  4. Environment:
    • the layer resolver (§4) with precedence and reserved-key validation;
    • container-only variables (yaml, repo settings, dialog editor, --container-env/--container-env-file), name-only -e injection, and the "Effective environment" view;
    • copying the .worktreeinclude files into the volume.
  5. Clone into a container at Add Project.
  6. Shared folder mode: spawn wrapper, mounts with read-only .git/hooks/config, hook relay, and git actions inside the container.
  7. Polish: sidebar badge, Restart/Recreate, resource templates, Podman parity, gVisor option, network none/allowlist.

7. Tests

  • Unit tests next to the WSL ones: src/main/wsl/wsl-orcad-host-transport.test.ts, wsl-orcad-deploy.test.ts.
  • Docker-gated tests like src/main/ssh/ssh-hostile-hosts.docker.test.ts.
  • A fake-transport e2e following tests/e2e/wsl-host-fake-transport.spec.ts (ORCA_E2E_FAKE_WSL_HOSTS → ORCA_E2E_FAKE_CONTAINER_HOSTS).
  • A CI image under config/docker/, using headless-pairing as the template.
  • A security regression test: run rm -rf "$HOME" /tmp/* (or similar) as the agent user inside a container workspace, then assert that the host home directory and the other workspaces are intact and the workspace can be recreated.
  • Environment tests:
    • layer precedence, and rejection of ORCA_* and other reserved keys;
    • the docker argv contains variable names only, and docker inspect shows no injected values;
    • a variable edited after creation reaches a newly opened terminal without a container recreate;
    • .worktreeinclude files land in the volume in container-only mode.

8. Open questions for maintainers

  • Core or bundled extension? ([Feature]: Add full docker support like git #12756 said Docker would come "via extension".) The transport, provisioning-in-main and cleanup hooks are core seams either way. The engine-specific part could ship as a bundled plugin, but the security defaults in §3 should not be optional.
  • On a headless Orca Server, should the in-container orcad be federated through the server (like runtime-owned SSH targets), or paired directly by each client?
  • Default mode: Container only (safest) or Shared folder (closest to today's workflow)?
  • Environment in volume mode: should per-terminal variables travel with the terminal create request, or should the in-container orcad pull them from the host?

Alternatives or additional context

Workarounds today, and why they fall short:

  • Cloud VM recipes with a local Docker image (skill-guides/orca-per-workspace-env/references/docker-ssh.md): desktop only, an sshd plus key plus port per container, no built-in recipe, and not available from the CLI or mobile.

  • orca-docker (https://github.com/talayolabs/orca-docker), a community provisioned-root recipe and the closest thing to this request today:

    • one retained container per Orca worktree, with nothing mounted (its own clone at the same absolute path);
    • key-only sshd per container, with the host key pinned into known_hosts over docker exec;
    • Orca's file tree, editor, git and terminals run inside the container over SSH, and work leaves through Orca's normal commit/push UI;
    • suspend/resume = docker stop/start, and destroy removes the container and its key.

    It proves the volume mode works with today's API. It also shows what core support would remove: the sshd, key and published port per container, the desktop-only limit ("Recipes run on the local Docker daemon"), and copying a recipe script into every repo. See §0 for a full comparison.

  • Docker Sandboxes as an SSH target ([Feature]: Support for Docker Sandboxes to Run Agents #13665, docs PR docs(ssh): document connecting to a Docker Sandbox #25892) and the orca-sbx-recipes plugin: microVM isolation, but one VM per project shared by every worktree. They need sbx and a Docker account, and are driven from the desktop.

  • Agent-level sandboxes (Claude Code sandbox, Codex workspace-write): configured per CLI, and they only cover some tools. The flag Orca launches Codex with by default turns Codex's sandbox off.

  • Running the whole orca serve in one container or VM: protects the host, but workspaces can still destroy each other.

Related issues and PRs:

Prior art:

I'm happy to test preview builds against a headless Orca Server setup.

Activity

  1. nwparker commented on Oct 16, 2026

    @nwparker
    Contributor

    @yoobi will ask you start a dicussion here

    Have folks ask for this but I think I need to hear that our users really care about this. For instance, many users already run in sandboxes so this is sorta a moot point.
    And I know many other users are compute constrained as it is.

    Doing this right is decent work so we'll hold of on changes here without getting community input

  2. locked and limited conversation to collaborators on Oct 16, 2026
  3. converted this issue into a discussion #29584 on Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions