Container environment as current user, in any image, preserving user identity.
Start container based on any image, with current directory mounted and runs as your own user in the container. Run a command or start an interactive shell.
# Install with pip
$ pip install ctenv
# Install with uv
$ uv tool install ctenv
# Or run directly without installing
$ uv tool run ctenv --helpRecommend installing uv.
- Python 3.10+
- Docker (tested on Linux and macOS)
# Interactive shell in ubuntu container
$ ctenv run --image ubuntu -- bash
# Run a configured container
$ ctenv run my-node
# Run a custom command and mount a volume
$ ctenv run my-node --volume ./tests -- npm testWhen running containers with mounted directories, files created inside often have root ownership or wrong permissions. ctenv solves this by:
- Creating a matching user (same UID/GID) dynamically in existing images at runtime
- Mounting your current directory with correct permissions
- Using
gosuto drop privileges after container setup
This works with any existing Docker image without modification - no
custom Dockerfiles needed. Provides similar functionality to Podman's
--userns=keep-id but works with Docker. Also similar to Development
Containers but focused on running individual commands rather than
persistent development environments.
Under the hood, ctenv starts containers as root for file ownership
setup, then drops privileges using bundled gosu binaries before
executing your command. It generates bash entrypoint scripts
dynamically to handle user creation and environment setup.
- Works with existing images without modifications
- Files created have your UID/GID (preserves permissions)
- Convenient volume mounting like
-v ~/.gitconfig(mounts to same path in container) - Simple configuration with reusable
.ctenv.tomlsetups
- User identity preservation (matching UID/GID in container)
- Volume mounting with shortcuts like
-v ~/.gitconfig(mounts to same path) - Volume ownership fixing with custom
:chownoption (similar to Podman's:Uand:chown) - Post-start commands for running setup as root before dropping to user permissions
- Template variables with environment variables, like
${env.HOME} - Configuration file support with reusable container definitions
- Cross-platform support for linux/amd64 and linux/arm64 containers
- Bundled gosu binaries for privilege dropping
- Interactive and non-interactive command execution
ctenv supports having a .ctenv.toml either in HOME or in project
directories. When located in a project, it will use the path to the
config file as project root.
Create .ctenv.toml for reusable container setups:
[defaults]
command = "zsh" # Run a shell for interactive use
[containers.python]
image = "python:3.11"
env = [
"MY_API_KEY", # passed from environment when run
"ENV=dev",
]
volumes = ["~/.cache/pip"]
Then run:
$ ctenv run python -- python script.pyUse containerized build environments:
[containers.build-system]
image = "some-build-system:v17"
volumes = ["build-cache:/var/cache:rw,chown"]Run linters, formatters, or compilers from containers:
$ ctenv run --image rust:latest -- cargo fmt
$ ctenv run --image my-node-env -- eslint src/Run Claude Code in a container for isolation with configuration for convenient usage:
~/.ctenv.toml:
# Run Claude Code in container
[containers.claude]
volumes = ["~/.claude.json", "~/.claude/"]
command = "claude" # Run claude directly
# Builds an image so you don't have to reinstall every time
[containers.claude.build]
dockerfile_content = """
FROM node:20
RUN npm install -g @anthropic-ai/claude-code
"""
Then start with: ctenv run claude
Basic example:
$ ctenv run --image node:20 -v ~/.claude.json -v ~/.claude/ --post-start-command "npm install -g @anthropic-ai/claude-code" -- claudeThat would install it every time you run it. To avoid that, we can use ctenv to build an image with Claude Code:
$ ctenv run --build-dockerfile-content "FROM node:20\nRUN npm install -g @anthropic-ai/claude-code" -v ~/.claude.json -v ~/.claude/ -- claudeYou likely want to configure this for conveniency:
# Run Claude Code in container
[containers.claude]
volumes = ["~/.claude.json", "~/.claude/"]
command = "claude" # Run claude directly
# Builds an image so you don't have to reinstall every time
[containers.claude.build]
dockerfile_content = """
FROM node:20
RUN npm install -g @anthropic-ai/claude-code
"""
If you have an existing image with a build environment already, use that and install Claude Code:
[containers.claude]
volumes = ["~/.claude.json", "~/.claude/"]
command = "claude"
[containers.claude.build]
dockerfile_content = """
FROM my-build-env:latest
RUN npm install -g @anthropic-ai/claude-code
"""and run with: ctenv run claude
ctenv by default mounts the current directory as "workspace" and switches to it, so it would start Claude Code in with the current directory mounted in the container.
If you don't already have an image with your development tools in
(node:20 doesn't include that much), you likely want to write a
Dockerfile and install more tools in it for Claude and you to use.
[containers.claude.build]
dockerfile = "Dockerfile" # instead of dockerfile_content
Can for example also use iptables to restrict network access:
[containers.claude]
# ...
network = "bridge"
run_args = ["--cap-add=NET_ADMIN"]
post_start_commands = [
"iptables -A OUTPUT -d 192.168.0.0/24 -j DROP",
]Note: On macOS, Claude Code stores credentials in the keychain by default. When run in a container, it will create ~/.claude/.credentials.json instead, which persists outside the container due to the volume mount.
Note: There are also other tools for running Claude Code in a container, such as devcontainers: https://docs.anthropic.com/en/docs/claude-code/devcontainer
Complex build environment with shared caches:
[containers.build]
image = "registry.company.internal/build-system:v1"
env = [
"BB_NUMBER_THREADS",
"CACHE_MIRROR=http://build-cache.company.internal/",
"BUILD_CACHES_DIR=/var/cache/build-caches/image-${image|slug}",
]
volumes = [
"build-caches-user-${env.USER}:/var/cache/build-caches:rw,chown",
"${env.HOME}/.ssh:/home/builduser/.ssh:ro"
]
post_start_commands = ["source /venv/bin/activate"]This setup ensures the build environment matches the user's environment while sharing caches between different repository clones.
-
Path handling in general
-
Config file: Relative paths are relative to the file.
-
Command line: Relative paths are relative to the current working directory.
-
-
A container configured in the current project config will shadow container with name defined in HOME/global config.
-
Project (
-p/--project)Specifies the project directory, the root of your project. Generally your git repo. Define the project by placing a
.ctenv.tomlthere, ctenv will look for it automatically.Supports volume syntax (
/project/dir:/project/mount) to specify also the project mount, i.e. where in the container the project directory should be mounted. Default is to mount at the same path as on the host. See also Workspace. -
Workspace (
-w/--workspace)Main directory to mount and use. Must be a subdirectory of the project directory. Default is the project directory.
Specify a subdirectory to limit which part of a project that gets mounted. (If multiple directories are needed, use
--volumefor the additional directories.)Will be mounted under the project mount. Example: If CWD is
/projectand ctenv is run with-p .:/repo -w ./foo, then/project/foowill be mounted at/repo/foo. -
Volume (
-v/--volume)Path to mount into the container.
Supports volume syntax (
/host/path:/container/path) to specify where in the container it should be mounted. Default is to mount at the same path as the host directory.Subpaths of the project directory will be mounted relative to the project mount. This is mainly useful when a specific Workspace has been specified, as it allows one to easily mount a subset of the paths of the project and have them all be mounted at the same paths under the project mount as if the entire project directory was mounted. Example: If CWD is
/projectand ctenv is run with-p .:/repo -w ./foo, then specifying-v ./barwill mount/project/barat/repo/bar(and/project/fooat/repo/foofor the workspace).
The background for ctenv was a bash script that I developed at work (Agama) for running our build system in a container. Besides running the build, it was useful to also be able to run and use the compiled code in the build system environment, which had older libraries than the modern OSes that was used by the developers.
ctenv is a much more generic tool than that bash script and without the many hard-coded parts. Written in Python and support for config files and much more.