
I Rebuilt the Devcontainer CLI in Rust. Here's Why.
The official devcontainer CLI assumes you live in VS Code. I built one for terminal-first, agentic coding workflows — with layered configs that eliminate repetitive setup.
Every time I started a new project, I copied the same .devcontainer/ folder, changed the image, added the same features, set the same environment variables, and wondered why I was still doing this manually.
Then I started coding with AI agents in the terminal — Claude Code, aider, the next wave of CLI-native dev tools — and realized the official devcontainer CLI assumes a world where VS Code is the center of everything. It isn't anymore.
So I built dev. A Rust-based devcontainer CLI that's spec-compliant, terminal-first, and designed around how I actually work now.
The Problem With Devcontainers Today
Devcontainers are great. Isolated, reproducible environments defined in code. The spec is solid. But the tooling has two blind spots:
1. Every project starts from zero.
You configure your preferred shell, your editor settings, your environment variables, your common features — for every single project. There's no concept of "these are my defaults, apply them everywhere." If you work across ten repositories, you maintain ten nearly-identical configs.
2. The workflow is VS Code-centric.
The official CLI exists, but it's built to support VS Code's Remote Containers extension. If you're working from the terminal — which is increasingly where AI-assisted development happens — you're fighting the tooling instead of using it.
What dev Does Differently
Layered Configuration
dev introduces a four-layer config system. Each layer merges into the next, so you define things once and they apply everywhere:
1. Global template ~/.dev/global/<name>/...
2. Base config ~/.dev/base/devcontainer.json
3. Runtime config ~/.dev/<runtime>/devcontainer.json
4. Per-project config .devcontainer/devcontainer.json
Base config is the foundation. This is where you put the things that should be true for every container you ever create — your preferred shell features, your git config, environment variables, default user. Run dev base new and it walks you through picking features from the devcontainer registry to build your baseline:
# Interactive setup — browse and select features for your base config
dev base new
# Or tweak it later
dev base edit
dev base config set remoteUser vscode
dev base config add features ghcr.io/devcontainers/features/common-utils:2
dev base config add remoteEnv EDITOR=vim
Global templates are reusable devcontainer setups organized by stack. I keep templates for rust, typescript, dotnet, golang, and others. Each one has the right base image, the right toolchain features, and the right lifecycle hooks for that ecosystem:
dev global new --name rust
dev global new --name typescript
dev global list
When you create a new project, you pick a template and dev merges everything together — base config into the global template, then any per-project overrides on top:
dev new --template rust
The merge is field-type-aware. Scalar values (like image or remoteUser) are overridden by higher-priority layers. Arrays (like mounts or forwardPorts) are concatenated and deduplicated. Maps (like remoteEnv) are merged with higher-priority keys winning. Features are unioned — you get everything from every layer.
The result: a new Rust project gets my standard shell setup, my git config, the Rust toolchain, and any project-specific additions, all composed automatically. No copying folders. No forgetting to add that one feature I always want.
Terminal-First Workflow
This is where dev diverges most from the VS Code workflow. Three commands cover the core loop:
# Spin up the container
dev up
# Drop into an interactive shell
dev shell
# Later, if you want VS Code
dev open
dev up builds the image (resolving features from OCI registries, running lifecycle hooks in the correct order), creates the container, and starts it. dev shell detects your preferred shell inside the container and opens an interactive session. dev open generates the right VS Code remote URI and launches the editor attached to the running container.
The order matters. You start in the terminal. You work in the terminal. If and when you want a GUI editor, it attaches to what's already running. VS Code is an option, not a prerequisite.
This maps directly to how agentic coding works. You have Claude Code or another AI agent running in a terminal session. You need an isolated environment with the right toolchain. You don't need to open VS Code first, wait for it to detect the devcontainer config, build the image, and attach. You need dev up && dev shell and you're working.
User-Scoped Recipes
Not every project needs a .devcontainer/ folder committed to git. Sometimes you want a devcontainer for a project that your team doesn't use devcontainers for.
dev new lets you choose between workspace scope (writes to .devcontainer/ in the project, committed to git) and user scope (stores a lightweight recipe at ~/.dev/devcontainers/<folder>/).
User-scoped recipes are minimal:
{
"globalTemplate": "rust",
"features": ["ghcr.io/devcontainers/features/zsh:1"],
"options": { "imageVariant": "bookworm" },
"rootFolder": "/path/to/workspace"
}
At build time, dev composes the full config from the recipe's referenced global template, the base config, and any overrides. The project directory stays clean. Your team doesn't need to know or care that you're running in a container.
Multiple Runtime Support
dev supports Docker, Podman, and — on macOS — Apple Containers via native XPC. Runtime detection is automatic, or you can specify it:
dev up --runtime podman
dev up --runtime apple
The runtime abstraction is trait-based in Rust, so each runtime implements the same interface. Switching between Docker and Podman doesn't require config changes.
The Full Command Set
# Scaffold and create
dev init # Minimal .devcontainer/ scaffold
dev new # Interactive template picker
dev new --template rust # From a specific template
# Build and run
dev build # Build the image
dev build --buildkit # With BuildKit optimization
dev up # Start the container
dev up --rebuild # Force rebuild
# Work
dev shell # Interactive shell
dev exec -- cargo test # Run a command
dev status # Check container state
# Manage
dev down # Stop the container
dev down --remove # Stop and remove
dev open # Open in VS Code
# Configure
dev config set image rust:latest
dev config add features ghcr.io/devcontainers/features/node:1
dev base edit
dev global new --name my-stack
dev global list
Why Rust
A tool that manages development environments probably shouldn't depend on one. The official devcontainer CLI is a Node.js project — which means to manage your containers, you first need a working Node installation, the right version, and npm install to behave. There's an irony in needing a functioning dev environment to set up your dev environment.
dev is a single static binary. No runtime, no package manager, no version conflicts. Drop it on a machine and it works. That matters most in exactly the scenario this tool is built for: you're on a fresh checkout, you don't have the project's toolchain installed yet, and you need to get into a container fast.
Rust also gives me compile-time guarantees on the config merge logic — the layered system that composes base, global, runtime, and project configs. That's the kind of code where a subtle bug means your container silently gets the wrong environment variables or missing features. The type system catches those problems before they become "why is my container broken" debugging sessions.
Who This Is For
If you're doing most of your development inside VS Code and the Remote Containers extension works for you, keep using it. It's good software.
But if you're:
- Using AI coding agents (Claude Code, aider, etc.) that need isolated environments from the terminal
- Tired of duplicating devcontainer configs across projects
- Managing multiple stacks and wanting reusable templates
- Working primarily from the terminal, with VS Code as optional
- Running containers on Podman or Apple Containers, not just Docker
Then dev might be worth a look. It implements the full devcontainer spec — OCI feature resolution, lifecycle hooks, variable substitution, lockfiles — so existing configs just work. It's open source, MIT licensed, and available on GitHub.
# Install from source (see GitHub for binary releases)
cargo install --git https://github.com/squirrelsoft-dev/dev
dev global new --name my-first-template
dev new
dev up
dev shell
Four commands from install to a running, configured development environment. No editor required.