ctenv is a tool for running in containers as current user
Project description
ctenv
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
# Install with pip
$ pip install ctenv
# Install with uv
$ uv tool install ctenv
# Or run directly without installing
$ uv tool run ctenv --help
Recommend installing uv.
Usage
# Interactive shell in ubuntu container
$ ctenv run --image ubuntu -- bash
# Run specific command
$ ctenv run -- npm test
# Run Claude Code in a container
$ ctenv run --image node:20 --volume ~/.claude.json --volume ~/.claude \
--post-start-command "npm install -g @anthropic-ai/claude-code"
Why ctenv?
When 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.
Highlights
- 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
Requirements
- Python 3.10+
- Docker (tested on Linux/macOS)
Features
- 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
Configuration
Create .ctenv.toml for reusable container setups:
[defaults]
command = "zsh"
[containers.python]
image = "python:3.11"
volumes = ["~/.cache/pip"]
# For running Claude Code in container
[containers.claude]
image = "node:20"
post_start_commands = ["npm install -g @anthropic-ai/claude-code"]
volumes = ["~/.claude.json", "~/.claude"]
Then run:
$ ctenv run python -- python script.py
$ ctenv run claude
Common Use Cases
Claude Code
Run Claude Code in a container for isolation:
$ ctenv run --image node:20 -v ~/.claude.json -v ~/.claude/ --post-start-command "npm install -g @anthropic-ai/claude-code" -- claude
Or write as config in ~/.ctenv.toml:
[containers.claude]
image = "node:20"
volumes = ["~/.claude.json", "~/.claude/"]
post_start_commands = ["npm install -g @anthropic-ai/claude-code"]
command = "claude"
and use with: ctenv run claude
You can also use Dev Containers for Claude Code: https://docs.anthropic.com/en/docs/claude-code/devcontainer
Development Tools
Run linters, formatters, or compilers from containers:
$ ctenv run --image rust:latest -- cargo fmt
$ ctenv run --image node:20 -- eslint src/
Build Systems
Use containerized build environments:
[containers.build]
image = "some-build-system:v17"
volumes = ["build-cache:/var/cache:rw,chown"]
Detailed Examples
Claude Code without installing every time
The most obvious way is to create a container image where you have installed Claude Code and run ctenv using that image.
[containers.claude]
image = "my-dev-image"
volumes = ["~/.claude.json", "~/.claude/"]
command = "claude"
One alternative is to use NVM and store the installation in a volume. Below is example just to showcase the NVM "hack". For real use, you likely want an image with more development tools installed.
In .ctenv.toml:
# Installing in volume called claude-nvm
[containers.claude-install]
image = "ubuntu:latest"
volumes = ["~/.claude.json", "~/.claude/", "claude-nvm:/nvm"]
env = ["NVM_DIR=/nvm"]
post_start_commands = [
# Install curl (for nvm)
"apt update && apt install -y curl",
# Install nvm
"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash",
# node and claude code
"/bin/bash -c 'source /nvm/nvm.sh && nvm install 20 && npm install -g @anthropic-ai/claude-code'"
]
command = "exit 0"
# Running
[containers.claude-run]
image = "ubuntu:latest"
volumes = ["~/.claude.json", "~/.claude/", "claude-nvm:/nvm"]
env = ["NVM_DIR=/nvm"]
command = "/bin/bash -c 'source /nvm/nvm.sh && claude'"
Run:
# Install (once)
$ ctenv run claude-install
# Run without installing again
$ ctenv run claude-run
Claude Code with Network Restrictions
For running Claude Code in isolation with network limitations:
[containers.claude]
image = "node:20"
network = "bridge"
run_args = ["--cap-add=NET_ADMIN"]
post_start_commands = [
"apt update && apt install -y iptables",
"iptables -A OUTPUT -d 192.168.0.0/24 -j DROP",
"npm install -g @anthropic-ai/claude-code"
]
volumes = ["~/.claude.json", "~/.claude"]
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.
Build System with Caching
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.
History
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 i Python and support for config files and much more.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ctenv-0.6.tar.gz.
File metadata
- Download URL: ctenv-0.6.tar.gz
- Upload date:
- Size: 1.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4693047c851cc6da29f6df3332487c4e8bcc76f612e73859b3aebc2d408eea0
|
|
| MD5 |
fdbb06721333630c785670b4a4a3c56f
|
|
| BLAKE2b-256 |
39e4e952869695909db0968074a32e42cdd9918d7b887ac511bdc86c42caf286
|
Provenance
The following attestation bundles were made for ctenv-0.6.tar.gz:
Publisher:
publish.yml on osks/ctenv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ctenv-0.6.tar.gz -
Subject digest:
d4693047c851cc6da29f6df3332487c4e8bcc76f612e73859b3aebc2d408eea0 - Sigstore transparency entry: 374044164
- Sigstore integration time:
-
Permalink:
osks/ctenv@1c234e6133068f81a77b2261f50290275224f4f6 -
Branch / Tag:
refs/tags/v0.6 - Owner: https://github.com/osks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1c234e6133068f81a77b2261f50290275224f4f6 -
Trigger Event:
release
-
Statement type:
File details
Details for the file ctenv-0.6-py3-none-any.whl.
File metadata
- Download URL: ctenv-0.6-py3-none-any.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13d98a50663b8bf4db05fdfe449adfa81661a72b48190e6b51a3b51e343d52dc
|
|
| MD5 |
6160b71a4f471c6942a664884a1e1e8e
|
|
| BLAKE2b-256 |
f3892a06ee0e1c0ee160321158c3cef8fdd711477851a1b62c77ae89836673a7
|
Provenance
The following attestation bundles were made for ctenv-0.6-py3-none-any.whl:
Publisher:
publish.yml on osks/ctenv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ctenv-0.6-py3-none-any.whl -
Subject digest:
13d98a50663b8bf4db05fdfe449adfa81661a72b48190e6b51a3b51e343d52dc - Sigstore transparency entry: 374044193
- Sigstore integration time:
-
Permalink:
osks/ctenv@1c234e6133068f81a77b2261f50290275224f4f6 -
Branch / Tag:
refs/tags/v0.6 - Owner: https://github.com/osks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1c234e6133068f81a77b2261f50290275224f4f6 -
Trigger Event:
release
-
Statement type: