mkenv is a CLI that builds and runs per-project developer containers using Docker with security-focused defaults, disposable sessions, and caching for language tooling. It exists for developers who would rather delete a container than reimage their laptop.
This documentation covers how to install, run, and configure mkenv. No YAML files required.
$PATH.brew tap 0xa1bed0/mkenv
brew install mkenv
# Download the binary (arm64 or amd64)
curl -L https://github.com/0xa1bed0/mkenv/releases/latest/download/mkenv-linux-arm64 -o mkenv
# or for amd64: curl -L https://github.com/0xa1bed0/mkenv/releases/latest/download/mkenv-linux-amd64 -o mkenv
# Make it executable and move to PATH
chmod +x mkenv
sudo mv mkenv /usr/local/bin/
mkenv version
Important: Ensure Docker is running before launching containers.
cd /path/to/your/projectmkenv .First run: Builds the full image and caches dependencies (slower)
Subsequent runs: Reuse cached layers for fast startup
While mkenv works with zero configuration, you can customize behavior using .mkenv files. These files provide the same options as command-line flags.
Create ~/projects/.mkenv to apply settings to all projects in that directory:
{
"enabled_bricks": ["codex", "nvim", "tmux"]
}
Create .mkenv in your project root:
{
"enabled_bricks": ["claude-code"],
"volumes": ["~/datasets:/data"]
}
Create .mkenv in your project root:
{
"extra_pkgs": ["git", "curl", "vim", "htop", "jq"]
}
These packages will be installed via the system's package manager (e.g., apt-get on Debian-based systems).
Inject environment variables into the container. Literal values are passed through; values prefixed with mkenv_value_from:<path> are read from a host file at container start.
{
"envs": {
"API_BASE": "https://api.example.com",
"API_TOKEN": "mkenv_value_from:~/secrets/api-token",
"DEPLOY_KEY": "mkenv_value_from:/home/me/.config/myapp/deploy.key"
}
}
Secret values resolved via mkenv_value_from: are never baked into the docker image and never logged — they are injected at container creation time and live only for the lifetime of the container.
Security guarantees for mkenv_value_from::
~/-prefixed. Relative paths are rejected.~/.ssh, ~/.aws, ~/.gnupg, /etc, /proc, browser/password-manager directories, or any other protected path. Symlink-escape attempts are blocked post-resolution.\n or \r\n) is trimmed; internal newlines are preserved so multi-line values such as PEM keys work.chmod 600.Reserved names: the MKENV_ prefix is reserved for mkenv-internal variables, and loader-hijack names (LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH) are blocked.
Note: custom envs are applied at container-run time, not at image-build time. Changing an envs entry does not trigger a rebuild — restart the container to pick up the new value.
.mkenv files found are loaded and merged (root → project).mkenv files| Field | Type | Description |
|---|---|---|
enabled_bricks |
array | Tools to install (e.g., ["codex", "claude-code", "nvim", "pulumi"]) |
disabled_bricks |
array | Tools to exclude from auto-detection |
extra_pkgs |
array | Additional system packages to install (e.g., ["git", "curl", "vim"]) |
volumes |
array | Additional directories to mount (e.g., ["~/data:/data"]) |
disable_auto |
boolean | Disable automatic language detection (default: false) |
envs |
object (string → string) | Environment variables to set in the container. Literal values are passed through; values prefixed with mkenv_value_from:<path> are read from a host file at container start (secrets never enter the image). See the example above for the security model. |
mount_gpg |
boolean | Forward host GPG agent into the container — equivalent to passing --mount-gpg on the command line. Auto-enables the gpg-agent brick. See GPG Agent Forwarding. |
Use cases: Set organization-wide defaults, team preferences, or project-specific requirements without repeating command flags.
Run AI coding tools safely inside mkenv containers using the --tools flag.
mkenv . --tools claude-code
claude-code — Anthropic's Claude Code CLIcodex — OpenAI's Codex CLI# Start container with Claude Code
mkenv . --tools claude-code
# Inside the container, run Claude
claude
AI tools run inside the container with restricted access:
~/.ssh, ~/.aws, ~/.config, or other credentialsInstall multiple tools at once:
mkenv . --tools claude-code,nvim,tmux
Other available tools: nvim, tmux, pulumi, ansible
If you use a YubiKey or smartcard with GPG, you can forward your host's GPG agent into the mkenv container. This lets you use YubiKey-backed SSH authentication, GPG signing, and file encryption/decryption inside the sandbox — just like on your host, without exposing your keys.
mkenv . --mount-gpg
Or set it permanently in your project's .mkenv file:
{
"mount_gpg": true
}
mkenv does three things when you pass --mount-gpg:
gpg --export) and imports them into the container. This works across all GPG versions (2.2 with kbx, 2.4+ with keyboxd). Private key stubs (smartcard references, no secret material) are also copied so GPG knows which keys live on your YubiKey.The result: gpg --list-secret-keys, gpg --encrypt, gpg --decrypt, gpg --sign, and SSH all work inside the container exactly as they do on your host.
enable-ssh-support in gpg-agent.conf)gnupg with gpg-agent runningBefore launching mkenv, tell the GPG agent which terminal to use for PIN prompts:
# Run this in the same terminal where you'll launch mkenv
gpg-connect-agent updatestartuptty /bye
Tip: Add an alias to your shell config to make this easier:
# In ~/.zshrc or ~/.bashrc
alias gpghere="gpg-connect-agent updatestartuptty /bye"
Then just run gpghere before mkenv.
All pinentry programs work with --mount-gpg:
pinentry-tty — plain text prompt, no curses rendering, works well in shared terminal environmentspinentry-curses — TUI box with ncurses, renders cleanly when mkenv pauses the terminalpinentry-mac — native macOS dialog, fully independent of the terminal# Terminal setup
gpghere
# Launch mkenv with GPG forwarding
mkenv . --mount-gpg
# Inside the container — SSH uses your YubiKey automatically
ssh root@your-server.com
# → PIN prompt appears on the host
# → Enter PIN, press Enter
# → SSH session opens
# Encryption/decryption works too
gpg --encrypt -r your@email.com secrets.env
gpg --decrypt secrets.env.gpg
# → YubiKey touch/PIN prompt on the host
# Signing
gpg --sign --armor message.txt
For infrastructure automation with YubiKey-backed SSH:
mkenv . --mount-gpg --tools ansible
This gives you a sandboxed environment where Ansible can SSH to remote hosts using your YubiKey for authentication. Run playbooks, manage inventory, and use ansible-vault — all isolated from your host.
Most GPG agent configurations cache the PIN after the first entry (this is the default). Ansible works seamlessly — the first SSH connection prompts for your PIN, and subsequent parallel connections use the cached PIN without any terminal disruption.
If you've disabled PIN caching or configured your YubiKey to require a touch for every operation, each SSH connection needs a separate PIN entry or touch. Ansible opens multiple SSH connections in parallel by default, which can cause overlapping PIN prompts.
Use --forks 1 to serialize connections so each gets a clean PIN prompt:
ansible-playbook -i inventory.yml deploy.yml --forks 1
Alternatively, set forks = 1 in your ansible.cfg:
[defaults]
forks = 1
Install Ansible inside your mkenv container for infrastructure automation:
mkenv . --tools ansible
This installs Ansible via pip along with its dependencies (python3, sshpass). All standard Ansible CLI tools are available: ansible, ansible-playbook, ansible-galaxy, ansible-vault, ansible-inventory, ansible-doc, etc.
Ansible's pip cache and ~/.ansible directory are persisted across container restarts.
# Infrastructure automation with hardware-backed SSH keys
mkenv . --mount-gpg --tools ansible
# Inside the container
ansible-playbook -i inventory.yml deploy.yml
Ansible SSH connections will use your YubiKey for authentication — PIN prompts appear on your host terminal when needed.
-h/--help for usage details.mkenv / mkenv runBuild (if necessary) and run a dev container for the current project.
mkenv run [PATH] [flags]
..| Flag | Type | Description | Default |
|---|---|---|---|
--tools |
comma list | Extra tools to preinstall (e.g. codex,claude,nvim,tmux,pulumi,ansible). |
none |
--langs |
comma list | Hint runtimes to prioritize (node,go,python,rust). | auto-detect |
--volume |
comma list | Mount additional host directories inside the container. Supports relative paths (e.g. "~/foo:~/foo") | project dir only |
--rebuild |
bool | Ignore cache and rebuild the image from scratch. | false |
--shell |
string | Preferred shell to launch inside the container (bash, zsh, fish). |
zsh |
--entrypoint |
string | Preferred entrypoint (e.g. "tmux") | none |
--mount-gpg |
bool | Forward host GPG agent into the container. Enables SSH, signing, and encryption/decryption via YubiKey/smartcard. Imports your public keyring automatically. | false |
--command / -c |
string | Run a single command inside the environment and exit (non-interactive). Stdout/stderr are streamed; the host process exits with the command's exit code. | none |
-c)Run a single command inside the mkenv environment without opening an interactive shell. Useful for CI pipelines, git pre-commit hooks, and one-off builds where the runtimes are not installed on the host.
# Run a single command and exit
mkenv . -c "make precommit"
# Use in a git pre-commit hook
mkenv . -c "npm test"
# Exit code from the command propagates to the host
mkenv . -c "exit 42"; echo $? # prints 42
mkenv attachOpen an additional shell in an already-running container for the same project.
mkenv attach [PATH]
mkenv listList all mkenv containers and their status.
mkenv list [--verbose]
--verbose includes cache and volume detailsmkenv cleanRemove containers and caches for a project.
mkenv clean [PATH] [--all]
--all: removes all mkenv containers and cachesmkenv completionGenerate shell completion scripts.
mkenv completion [bash|zsh|fish]
Add the output to your shell config for tab completion.
mkenv helpShow help for commands.
mkenv help [command]
mkenv automatically detects your project's languages and tools by scanning for:
package.json, pnpm-lock.yaml, yarn.lockrequirements.txt, poetry.lock, Pipfilego.modCargo.tomlGemfileBased on what it finds, mkenv installs the appropriate language runtimes, package managers, and language servers.
mkenv caches dependencies using Docker volumes. First run builds everything, subsequent runs are fast. Rebuilds only happen when dependency files change.
mkenv scans your project directory for sensitive files (SSH keys, tokens, credential files) before starting. If it detects anything suspicious, it will warn you and ask for confirmation.
Best practice: Never mount sensitive credentials into the container. Keep secrets on your host machine only. Remember that the container could be compromised by a supply chain attack.
Run your dev server normally inside the container:
npm run dev
mkenv automatically routes localhost:3000 (or any port) from your host machine to the container. Access it from your browser, Postman, or any other tool as usual.
Unlike vanilla Docker or Devcontainers, you can't dynamically expose ports from inside a Docker environment — you need to think of exposed ports in advance. mkenv solves this with a bidirectional reverse proxy.
┌──────────────────┐
│ Browser │
│ │
│ connects to │
│ localhost:3000 │
└────┬─────────────┘
│
│ ┌──────────────────────┐
│ │ mkenv host process │ ┌──────────┐
└────────────► │ │ Postgres │
│ Binds :3000, :5432 │────► :5432 │
│ on Mac on demand │ └──────────┘
└───────────▲──────────┘
│
│
:45454 (fixed port)
┌─────────────────┼────────────────────┐
│ Docker │ │
│ │ │
│ ┌────────────▼──────────────┐ │
│ │ mkenv Container │ │
│ │ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ Proxy Daemon │ │ │
│ │ │ :45454 :5432│ │ │
│ │ └─────────┬──────────▲─┘ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ ┌─────────▼──────────┼─┐ │ │
│ │ │ npm run dev :3000 │ │ │ │
│ │ │ │ │ │ │
│ │ │ connects to │ │ │ │
│ │ │ localhost:5432 ───┘ │ │ │
│ │ └──────────────────────┘ │ │
│ └───────────────────────────┘ │
│ │
└──────────────────────────────────────┘
The mkenv host process acts as a bidirectional traffic hub between your host and the container, using a fixed port (:45454) to avoid Docker's dynamic port limitation.
mkenv . and dies when you exitmkenv includes a comprehensive policy engine that enforces security guardrails at multiple levels. Policies can be configured globally or per-project using a policy.json file.
Secret Detection:
Restricted Directories:
~/.ssh, ~/.aws, ~/.config from being mountedCreate a policy.json file in your mkenv config directory (typically ~/.mkenv/policy.json) to enforce organizational policies:
{
"disabled_bricks": ["codex"],
"enabled_bricks": ["nvim", "tmux"],
"allowed_mount_paths": ["/home/user/projects", "/data"],
"allowed_project_path": "/home/user/projects",
"ignore_preferences": false,
"reverse_proxy": {
"denied_ports": [5432, 3306],
"allowed_ports": []
}
}
| Field | Type | Description |
|---|---|---|
disabled_bricks |
array | Block specific tools from being installed (e.g., ["codex", "claude-code"]) |
enabled_bricks |
array | Force-enable specific tools regardless of project detection |
allowed_mount_paths |
array | Whitelist of directories that can be mounted (empty = allow all except blocked) |
allowed_project_path |
string | Restrict where mkenv can run (empty = allow all) |
ignore_preferences |
boolean | Override user preferences with policy settings |
reverse_proxy |
object | Control which host ports containers can access (see Reverse Proxy Security below) |
0444 permissions (read-only). mkenv will refuse to start if the policy file has incorrect permissions. This prevents unauthorized modification of security policies.
chmod 444 ~/.mkenv/policy.json
mkenv allows containers to access host services (like databases) via a reverse proxy. The policy engine strictly controls which ports can be accessed.
The following ports are permanently blocked and cannot be overridden by policy configuration:
{
"reverse_proxy": {
"denied_ports": [5432, 3306],
"allowed_ports": [8000, 8080]
}
}
denied_ports - Additional ports to block beyond hardcoded listallowed_ports - Explicit allowlist (if set, only these ports are accessible, but hardcoded denials still apply)Policies can control which tools (called "bricks") are available:
{
"disabled_bricks": ["claude-code", "codex"],
"enabled_bricks": ["nvim", "tmux", "pulumi"]
}
Use Cases:
{
"allowed_mount_paths": [
"/home/user/projects",
"/data/shared"
]
}
allowed_mount_paths is set, only directories under these paths can be mounted~/.ssh){
"allowed_project_path": "/home/user/approved-projects"
}
{
"disabled_bricks": ["codex"],
"enabled_bricks": ["nvim"],
"allowed_project_path": "/work/projects",
"allowed_mount_paths": ["/work"],
"ignore_preferences": true,
"reverse_proxy": {
"denied_ports": [5432, 3306, 6379, 27017],
"allowed_ports": []
}
}
This policy:
/work/projects directory only/work{
"disabled_bricks": [],
"enabled_bricks": ["claude-code", "nvim", "tmux"],
"reverse_proxy": {
"denied_ports": [],
"allowed_ports": []
}
}
This policy:
Yes. Linux is fully supported (arm64 and amd64). Windows works via WSL2 using the Linux binary.
No, it depends on Docker for virtualization. Think of mkenv as the paranoid orchestrator on top.
In Docker volumes managed by mkenv. Use mkenv list --verbose to inspect cache details.
Run mkenv . again. You'll either reattach or get a fresh container using existing caches.
No. It's a dev tool for local workstations, not a deployment platform. If you push it to prod, that's on you.
Yes. You can connect VS Code or Cursor to the mkenv container using their remote development features. This keeps the editor UI on your host while running all commands inside the container.
No analytics pings. If something breaks, file an issue rather than letting logs spy on you.
Git operations (push, pull, clone) happen on your host machine where your credentials live. Everything else happens in the sandbox. This separates concerns: work runs isolated with no credentials, git operations run on host with credentials and no bloat.
Use mkenv sandbox install <pkg> from inside the container. This is a controlled, audited way to install system packages. You can also specify packages in your .mkenv file with extra_pkgs.
Guide it with CLI options (--langs, --tools), a .mkenv file, or adjust ad-hoc with mkenv sandbox install.
VS Code and Cursor can connect to mkenv containers using their remote development features. Dedicated plugins coming soon.
Yes. Use --mount-gpg to forward your host's GPG agent into the container. SSH, GPG signing, encryption, and decryption all work — your public keyring is imported automatically and all crypto operations are forwarded to the host agent. Run gpg-connect-agent updatestartuptty /bye before launching mkenv so the agent knows where to show PIN prompts. See GPG Agent Forwarding for full details.
Yes. Use --tools ansible to install Ansible. Combine with --mount-gpg for YubiKey-backed SSH to remote hosts. See Ansible for details.