This picks up where DevPod: SSH-Based Devcontainers Without IDE Lock-in left off.

I’ve been hacking on dev environment tooling since 2013. Ventriloquist and devstep were different takes on the same problem: I want to git clone && cd into a project to start working on it without thinking about infrastructure.

I think I found out about devcontainers back when the CLI got first released (early 2022 maybe? Hard to tell by now), it felt like the spec I’ve been waiting for. A JSON file describes the environment, tooling builds the container. That’s very similar in spirit to devstep’s buildpack approach but standardized and supported by a real ecosystem.

The thing preventing me from using it was the fact that for a good while the way to use it with a nice UX was through VS Code, I didn’t want to leave nvim to go there, but I got into it anyway with Cursor last year.

Then DevPod changed that, I could SSH into a container and use whatever editor I wanted, done. That’s something I was comfortable with since the old Vagrant days and I even wrote a post about it last year, but early this year things started going sideways.

I’ve been meaning to experiment with Podman for a while due to its daemonless / rootless support. I was trying to get DevPod working with Podman and couldn’t get it to work, I unfortunately won’t recall the details right now, what matters is that I went to the repo looking for help, and the last commit was… a merge of my own PR from 4 months ago fixing some port forwarding docs. I poked around the issues and remembered that @nick-walt pointed out to me that a community fork was being maintained so I gave it a shot.

Somehow my git signing workarounds stopped working after switching to the fork, not sure if related. I only caught it when I noticed unsigned commits on a PR and couldn’t be bothered troubleshooting, just went back to plain old bind mounts for SSH keys / agent forwarding as a workaround.

And that’s when I started considering more seriously the idea of building my own tooling again which eventually became crib.

Why not just use the official CLI? Or fork DevPod?

I never really considered the official devcontainers CLI TBH. I played with it in the past and remembered that it required Node, I just wanted a single binary I could just drop in. Later I found out that these days they have an install script that downloads Node for you and wires things up behind the scenes, but by the time I found that out I was already past it.

The official CLI is a mature reference implementation but it’s not the kind of project that’s going to move fast on new ideas. I’ve been missing the plugin/extension capabilities I had back in my devstep days (I even had a Squid proxy plugin for caching package downloads) and the official CLI isn’t built for that.

The community DevPod fork worked mostly fine when I tried it but I was already leaning toward something leaner, a smaller core that I could extend over time so I switched back to “scratch my own itch” mode.

Before fully committing, I checked one thing: could I just fork DevPod itself (or fork the fork) and strip it down? I pointed Claude at the original loft-sh/devpod codebase and asked what it would take to extract the essentials. It ran for about 30 minutes and came back with a long list of things to remove: providers, SSH tunnels, agent injection, gRPC, IDE integrations. Not sure if Claude hallucinated but they were all “tangled together” which was kinda scary.

Claude also flagged the licensing. DevPod is MPL-2.0, which is file-level copyleft, meaning every source file you modify carries the license with it. That’s not a dealbreaker on its own but starting from scratch with good old MIT was just cleaner.

TL;DR - ripping things out or trying to push my ideas into the official CLI was probably going to take longer than starting fresh.

How it came together

crib wasn’t a weekend of agentic coding, I actually had a first attempt back in September 2025 that used the Docker Go SDK directly which kinda worked, but it was a lot of code between me and a running container so I just threw it away. Maybe it would have been different with a newer model like Opus 4.6, dunno.

For crib, I started differently though. I asked Claude on my phone to distill the devcontainer spec into something digestible, got that markdown over to the computer, fed it into Claude Code along with a round of research on the reference implementation source code, and ran my project-inception skill (Claude interviews me about what I want to build and then produces docs + roadmap from that). From there, Claude executed the plan while I reviewed code, steered architecture decisions and did manual testing on my work project.

Go was a conscious choice: I’ve got experience with it and it compiles to a single binary. Also, instead of the SDK approach, crib just shells out to docker or podman and lets them “have fun with my --args”.

What crib does

crib reads a devcontainer.json, builds the image, runs lifecycle hooks, and crib shell drops you in. I want to keep the core small and expand once it’s battle tested.

The design choices came from things that gave me a hard time with DevPod. The git signing wrapper bug was enough to convince me that the simplest path between me and a shell is docker exec without SSH or agents injected into the container. This is how I used to work with containers back in the vagrant-lxc / devstep days way before devcontainers were a thing.

I wanted to cd into a project and go, no flags to remember. The workspace names come from the current directory, just crib up, crib shell, done. Docker and Podman (including rootless) are auto-detected as runtimes or can be set via CRIB_RUNTIME.

For a detailed comparison with the official CLI and DevPod, there’s a comparison page in the docs.

Using it for real

I started using crib on my work project by around late February. Within hours things broke and I had to rollback to DevPod. Lifecycle hooks weren’t re-running when I expected, and there was a massive amount of duplicated code that resulted in fixing things while breaking other things, whackamole style.

Claude had implemented the hook markers on the host side, so stop + up looked like the container was already set up. That led to renaming stop to down (which removes the container and clears markers) and eventually to snapshot-based restarts so I wouldn’t lose software installed by hooks.

Variable interpolation bit me as well. My devcontainer.json used ${PATH} in remoteEnv (instead of the spec-standard ${containerEnv:PATH}) and crib was passing the literal string instead of resolving it, which overwrote the container’s real PATH. The kind of stuff you only find when you stop testing against toy configs.

Environment probing was also tricky. crib probes the container’s shell to capture $PATH and other vars so crib shell sees the right tools. But Claude had it running before lifecycle hooks, so anything installed by hooks (like mise install in my setup script) wasn’t on PATH, I’d open a shell and ruby just wasn’t there. The fix was to probe again after hooks, but then mise’s internal state variables leaked into the saved environment and confused mise in new shells. Every fix surfaced the next bug and I almost gave up TBH.

A lot of these bugs showed up twice, once for single-container workspaces and once for compose. The two paths had enough differences that a fix for one didn’t always cover the other. For example, compose restart used docker compose restart, which doesn’t start stopped dependency services, so my database container would stay down. Plugin env vars survived restarts on single-container but got silently dropped on compose. I ended up doing a “backend abstraction refactor” to try and consolidate the two paths, which helped, but it’s still an ongoing effort.

Using crib on my work project and other stuff I’m cooking (like please-make-me-an-app) felt like the first time devstep came together back in the day. cd into the project, crib up, crib shell, and I’m in a working environment that I configured once and don’t think about anymore.

Some extras

The thing I missed most from devstep was opinionated defaults that work everywhere without per-project boilerplate so I gave crib a plugin system (all baked into the binary for now). Built-in plugins handle SSH agent forwarding, shell history persistence, package caching a la vagrant-cachier, and coding agent credentials.

They all run automatically at container creation (except for the caching one which requires configuration) and fail open, if the SSH agent isn’t running or there are no Claude credentials the plugin just skips without errors.

The Claude credentials plugin workspace mode was not planned. Coincidence or not, my company recently signed up for a Claude Team plan so I added a way to use my work email credentials inside work containers instead of my personal ones which get mounted into other containers by default.

crib restart wasn’t planned either. It came from getting annoyed at unnecessary rebuilds while dogfooding. It diffs the devcontainer config against the stored one and picks the minimal action. No changes? Just docker restart. Safe changes like volumes, mounts, or env vars? Recreate the container without rebuilding the image. Image-affecting changes like the Dockerfile or features? It tells you to crib rebuild instead. It might not cover all scenarios but does the trick for me most of the time.

The part I’m still thinking about

The majority of code I “wrote” for crib was markdown. Basically lots of prompts, fine tuning specs / project inception docs. Claude wrote like 99% of the Go code and yet it feels like mine in a way I didn’t expect.

I knew what to build because I’ve been obsessing tinkering about this same problem for many years. The opinions that shaped crib came from years of building my own and experimenting with other things, and those opinions haven’t changed even though I’m not writing much of the Go myself anymore.

There’s definitely code in crib I won’t be able to explain line by line without reading it first. That’s a new feeling that I’m adapting to, but the tool does exactly what I wanted, shaped by decisions I’ve been forming since 2013 and I think that’s what really matters.

Le Future

The space keeps shifting as usual… Nix-based tools take a different approach to the same problem, and agentic coding is making IDEs less central after each release. crib is my answer for now, it’ll probably look different in another decade, or maybe next week? Who knows 🙈

If you want to try crib or poke around the code: check crib on GitHub and / or go read the docs 🚀