dcs (Dev Container Sandbox) runs commands in disposable Dev Containers.
Each invocation names zero or more components. A component contributes a base image, an optional Dev Container Feature, and optionally the command to run. dcs starts an isolated container, runs the first component that carries a command (or the command you choose), and removes the container when it exits. Source files and selected agent/terminal settings are shared with the host.
- Linux or WSL2 with an accessible Docker Engine
- Bash 4.4+,
make, and thedevcontainerCLI npmifmake installneeds to install the CLI- The published Features from devcontainers/features and temeteke/devcontainer-features
From a checkout of this repository:
make install # dcs, its bash completion and, if missing, the Dev Container CLI
make install-dcs # dcs and its bash completion only
make uninstall # remove dcs and its bash completion
make uninstall-devcontainer # remove the npm-managed Dev Container CLIdcs is installed to ~/.local/bin; set BINDIR to change that (for example,
make BINDIR=/your/bin install). make install leaves an existing
devcontainer on PATH unchanged; otherwise it installs @devcontainers/cli
using npm's current global prefix. Ensure ~/.local/bin and the npm global bin
directory are on PATH.
The bash completion is installed to
~/.local/share/bash-completion/completions/dcs; set BASHCOMPLETIONDIR to
change that. bash-completion loads it when a new shell starts, and
make uninstall-dcs removes it together with the command.
make uninstall never removes the Dev Container CLI. Use the same BINDIR as
at install time; remove a CLI installed by another package manager with that
manager.
From the project directory you want to edit:
dcs opencode # opencode
dcs pi # Pi Coding Agent
dcs opencode,tmux,vim # with tmux and Vim
dcs python,node # Python image + Node Feature
dcs python,node bash # both languages, in a shell
dcs python:3.12 # versioned Python image
dcs node,python:3.12 # Node image + Python 3.12 Feature
dcs base bash # run a shell on the plain base image
dcs kubectl,helmfile # Kubernetes tools (see host requirements)
dcs --mount-dir ../shared-lib python,node # add another host directory
dcs -p 8080:80 python,node # publish a container port on the host
dcs opencode opencode --help # pass options to opencodeOptions come before the component list. Everything after the list is the
container command, replacing the default. A component is required before a
command; use dcs base COMMAND for a command on the plain base image. dcs
alone starts a bash shell. dcs --help is the wrapper's help only before the
component list. Name the command yourself to pass options to a component's
default command.
dcs --image mcr.microsoft.com/devcontainers/python:3.12 \
--feature ghcr.io/devcontainers/features/go:1 codex--image pre-sets the image, so a language named later is added as a Feature.
--feature is repeatable and takes a full Feature reference. The bundled
Features require a Debian/Ubuntu-compatible image with apt-get. Long options
that take a value accept both --option VALUE and --option=VALUE; short
options take the value as the next argument.
make install-dcs installs Bash completion for options, components, exported
environment variables, and --mount-dir directories. It asks the installed
dcs for candidates, so adding a component to bin/dcs needs no regeneration.
dcs pyth<TAB> # python, python:3.12, ...
dcs python,no<TAB> # python,node, python,node:22, ...
dcs --mount-dir ../sha<TAB> # matching directories
dcs --mount-dir ~/sha<TAB> # matching directories under HOME
dcs --env ANTH<TAB> # exported host variable names--mount-dir completion returns directory paths. The separate unquoted
~/... form is completed as an expanded absolute path, and bare ~/~user
completes login names. --mount-dir=~/... and a quoted tilde-prefix produce
no candidates; ~+, ~-, and ~N are also not completed. See
Additional directories for runtime handling.
After the component list, Bash's normal completion is used.
Without bash-completion, load the same function from your shell startup file:
eval "$(dcs --completion bash)"After upgrading dcs, reinstall or reload the completion script:
make install-dcs, or eval "$(dcs --completion bash)" in the current shell.
--env NAME (or -e NAME) forwards a host environment variable to the
container; --env NAME=VALUE sets a literal value. The option is repeatable,
and a later value for the same name wins. A missing or unexported host variable
is an error.
export ANTHROPIC_API_KEY=...
dcs -e ANTHROPIC_API_KEY claude
dcs --env FOO=bar opencodeOptions come before the component list. Values are available to the command
and to lifecycle hooks, and disappear with the container. Prefer the NAME
form for secrets: NAME=VALUE appears in shell history, the dcs process
arguments, and the ephemeral config, while NAME is resolved from the host
environment without writing the value to the config. A literal value
containing ${...} may be expanded by the Dev Container CLI; use the NAME
form in that case. Avoid forwarding PATH or HOME unless intended; they
replace the container's values and can break the command.
--mount-dir DIR[:TARGET] bind-mounts another host directory into the
container. The option is repeatable and must come before the component list.
DIR is relative to the current directory or absolute. dcs does not expand
~: the shell expands it only in the unquoted separate --mount-dir DIR form,
while --mount-dir=DIR keeps a leading ~ literal (the shell still expands
$HOME). Use an absolute path, $HOME/... for the home directory, or
./~/... for a literal directory named ~. This differs from earlier dcs
versions, which expanded ~ in every form. The directory must exist. It is
mounted at /workspaces/<TARGET>, where TARGET is a single name directly
under /workspaces. Without TARGET, the directory's basename is used after
resolving symlinks.
dcs --mount-dir ../shared-lib python,node
dcs --mount-dir ../a/lib:lib-a --mount-dir ../b/lib:lib-b opencodeThe mount is writable; changes are made on the host and survive container
deletion. There is no read-only mode. The host root and the workspace mount
source (the Git root when applicable) are refused, as are files and paths that
do not exist. A TARGET that is already used by the workspace or another mount
is rejected.
In the container, the mount is at /workspaces/<TARGET>. When dcs runs in a
Git repository, the workspace is mounted from its Git root. If it is run from a
subdirectory, the container's workspace folder is
/workspaces/<repo>/<subdir>, so ../<TARGET> may not resolve to the mount.
Use /workspaces/<TARGET> or adjust the relative path.
TARGET cannot contain /, :, ,, ", ${...}, or control characters,
cannot be . or .., and cannot start or end with whitespace. DIR cannot
contain ,, ", ${...}, or control characters, and cannot start or end with
whitespace. Internal spaces are allowed in both. ${...} is rejected because
the Dev Container CLI would expand it in the generated config. DIR and
TARGET are separated by the last :, so a DIR containing : needs an
explicit TARGET. If the whole DIR:TARGET string is also an existing
directory, dcs refuses the ambiguous mount; choose a different TARGET or
add another :TARGET. Docker's -v, --volume, and --mount syntax is not
supported; use this option's DIR[:TARGET] form. A DIR starting with --
cannot use the separate --mount-dir DIR form because it looks like an option;
use --mount-dir=DIR or prefix the path with ./. Each mount widens the
sandbox; mount only the directories you need.
-p SPEC (or --publish SPEC) publishes a container port using the same
syntax as Docker's -p. The option is repeatable and must come before the
component list; -P/--publish-all is not supported.
dcs -p 8080:80 python,node
dcs -p 127.0.0.1:8080:80 opencodeName both ports for a fixed host port; a bare container port such as
-p 3000 gets a random host port, as in Docker. See Docker's -p
documentation for the supported forms, including address, port ranges and
protocols. The value is passed to Docker unchanged; an invalid value is
reported when the container is created, after the image is built or pulled. A
value containing ${...} is rejected because the Dev Container CLI would
expand it in the generated config.
base provides the plain mcr.microsoft.com/devcontainers/base:ubuntu24.04
image, with no Feature or command. It is appended unless named.
The first argument is a comma-separated list. Components are processed in order:
the first image wins, later components are added as Features, and the first
command runs (bash if none does).
| Language | Pinned versions | Command |
|---|---|---|
python |
3.14, 3.13, 3.12, 3.11, 3.10 | python |
node |
24, 22, 20 | node |
go |
1.27, 1.26, 1.25 | — |
rust |
1.87, 1.86, 1.85 | — |
java |
25, 21, 17 | — |
dotnet |
10.0, 9.0, 8.0 | — |
ruby |
3.4, 3.3, 3.2 | — |
php |
8.5, 8.4, 8.3 | — |
cpp |
— | — |
The remaining components are Feature-only (default command in parentheses):
opencode (opencode), pi (pi), codex (codex --no-daemon), claude (claude),
tmux (tmux), vim (vim), playwright, gitlab-cli, github-cli,
kubectl, helmfile, git-config.
The bare name uses the latest image; a versioned component such as
python:3.12 uses the image tag when it comes first and the Feature's version
option when layered. Rust version components are Feature-only; cpp is
image-only and must come first. An image-only component that follows an
image-setting component is rejected.
Language images come from mcr.microsoft.com/devcontainers/<language>:<tag>
(node uses javascript-node); each language except cpp also has a matching
ghcr.io/devcontainers/features/<language> Feature. The tool Features come from
ghcr.io/temeteke/devcontainer-features: the agent, terminal and Kubernetes
tools, gitlab-cli, github-cli, and git-config. github-cli wraps the
official Feature and persists ~/.config/gh; gitlab-cli and github-cli
also install git-config as a dependency. Run dcs --help for the exact tags,
Feature versions and options.
Some of these Features bind-mount host files and need them to exist before the container starts:
| Component | Host path |
|---|---|
kubectl |
~/.kube |
github-cli |
~/.config/gh, ~/.gitconfig |
gitlab-cli |
~/.config/glab-cli, ~/.gitconfig |
git-config |
~/.gitconfig |
Create them first, for example:
mkdir -p ~/.kube ~/.config/gh ~/.config/glab-cli
touch ~/.gitconfigdcs does not check these paths; a missing source surfaces as a container
creation error.
Adding a component, a version or a Feature option means adding one
add_component line in bin/dcs; its command is an argv, so it may carry fixed
arguments.
- Disposable: Every call gets a unique
dcs.sessionlabel and its container is removed at exit or on normal signals. Built images remain cached; packages installed at runtime are discarded with the container. - Workspace: The current directory is passed as
--workspace-folder;dcsleaves the CLI's--mount-workspace-git-rootdefault in place.--mount-diradds writable bind mounts directly under/workspaces. - Ports:
--publishadds each value to the ephemeral config'sappPort, which the Dev Container CLI passes todocker run -punchanged. - Configuration: An ephemeral
devcontainer.jsonis generated outside the repository; existing.devcontainerfiles are ignored and never modified. - Lifecycle:
devcontainer upbuilds and starts the container and applies Feature lifecycle hooks;devcontainer execruns the command;docker rm -fremoves only this session's container.
The wrapper does not implement persistent containers or .devcontainer
generation. Use Dev Container CLI and Templates for persistent environments.
Some Features bind-mount host files and directories. A bind-mount source must
exist before container creation; dcs does not check the paths used by
Features, so a missing one surfaces as a container-creation error. Mounted
paths are writable from the container and changes survive container deletion.
See each Feature's documentation for the paths it uses. --mount-dir sources
are checked before the container starts.
The host paths listed under Components can contain sensitive data: ~/.kube
holds cluster credentials and client certificates, and ~/.config/gh,
~/.config/glab-cli and ~/.gitconfig hold tokens or credential
configuration. git-config filters host-specific settings from the generated
container config, but the mounted host ~/.gitconfig itself remains writable.
Enable these components only for projects you trust.
The sandbox limits accidental access to files outside the workspace. It is not
a security boundary for untrusted or malicious code: credentials and state may
be shared, and the container retains network access. Environment variables
passed with --env are visible to container processes; the NAME form keeps
the value out of the ephemeral config, but the container is not a security
boundary. --mount-dir exposes additional host directories; mount only the
directories you need. --publish exposes container ports on the host; without
an explicit IP they listen on all host interfaces and can be reached by other
machines, so bind to 127.0.0.1 when the port is only for local use. Do not
bind-mount the Docker socket or host root into the container; dcs does not do
so itself, and --mount-dir refuses the host root.
It does not detect a Docker socket inside a mounted parent directory such as
/var/run or /run; avoid mounting those directories.
SIGKILL, host shutdown or Docker failures can leave a container running. Check
docker ps -a --filter label=dcs.session and remove only stale dcs containers.
Run the offline tests (Docker and Dev Container CLI are mocked):
make testThese tests validate option handling and lifecycle/cleanup behavior, not a real Docker container or tmux/Vim Feature installation. Run an integration check on your actual Linux/WSL2 Docker host before relying on this for daily development.