For the past couple of months, I have been trying to find an easy to use, performant and secure way of isolating my dev environment from the rest of my system.
My threat model is simple: I would like to guard against transitive
dependencies that attempt to do the simplest stuff: steal my credentials
from dotfiles in my home directory, try encrypting my home directory for
ransomware etc. I don’t expect any of this to guard me against some
state-sponsored operations, and neither Docker nor bubblewrap are
suitable for such purposes. However, I feel like we should not be
defeated by an array of lazy curl POST commands containing
our tools’ tokens. That just seems like the software engineering
equivalent of keeping our doors locked.
The problematic part has always been maintaining and keep using the same system over and over again. The best solution I used was a bubblewrap environment (see https://git.sr.ht/~gotlou/code-sandbox), but I gradually removed it from daily usage for various reasons:
using bwrap + direnv to open an unprivileged shell for running every command was a decent solution but required a lot of language-specific changes to make sure that the isolation was decent and it did not end up saving any artifacts to the home directory (eg. Rust, Go do this)
Having it around common apps like VLC and Okular was fine at first, but later I ran into weird errors when I tried to run them from KRunner/the Plasma GUI. I have no idea why this was happening, and at the time I just wanted to watch some video/read a PDF, so this was incredibly annoying.
(Note: I still do use this approach for Mullvad Browser, since it already takes great pains to be as uniform as possible for defeating fingerprinting. It has now become my primary browser to open untrusted pages. Thanks to me not giving it certain resources, I can 100% say it will NOT have access to certain classes of information, making it impossible to leak)
I eventually ended up stripping back most of the protection from my system for development environments due to friction: I was spending more time fighting with Nix, or bubblewrap or a combination of the two rather than being able to set up open source projects, build them locally and learn from them.
I continued expanding my sandbox efforts in the system services my desktop was running through systemd’s sandboxing capabilities, but this problem was left largely unsolved.
However, some of this fighting led me to probably the best solution for me.
For the Ladybird browser, I was not really happy with using the nix development shell. The nixpkgs definition patches a bunch of stuff, leading to me building a browser differently from how the rest of the development community does it, which could lead to issues later on. Running vcpkg and the upstream build tooling would inevitably fail due to NixOS-isms, which I was unwilling to part with.
Thus, the easiest solution seemed to be to just use a Docker image to build and run Ladybird inside, which led to me creating ladybird-docker-dev. Later on, I used some of this work inside servo-docker-dev, to sandbox Servo as well.
I would clone the upstream repo (like Ladybird or Servo) inside these -docker-dev repos (called wrapper repos) and from there on, these three components would work together in harmony to do whatever I wanted, inside an isolated environment.
These three main parts were:
a Dockerfile to house all the dependencies needed to build and run the project
a container.sh file which would drive the Docker container and create ephemeral containers to build/run/test the project, depending on what I passed in, and an option to just run a shell inside the container for debugging/running custom commands/ having a REPL etc
This also sets up mounts for the upstream repo code, and any cache dirs (.ccache for ccache, .cargo for Rust, etc, which are all mapped to hidden folders inside the wrapper repo)
This approach served me very well. There were some standard command line shortcuts to run, build or test these projects easily without me having to remember any project-specific stuff. Whenever a project moved forward and added a new dependency, I just had to add it to the Dockerfile, and it was permanently carried forward, and there was a written record of it being present in my environment (similar to my NixOS setup). If a port needed to be forwarded, I had to just modify my container.sh file to expose it.
This approach is not new. Microsoft has the devcontainers, and
docker-compose is also pretty old. However, they are not
present everywhere, and in case of devcontainers, it is very heavily
VSCode-oriented, with a middling CLI interface. I like my approach more,
as it is much simpler and can be made to work in a wider variety of
setups. It is simpler to understand and maintain, requiring knowledge of
only basic Docker and bash.
Most importantly, it is independent of the upstream project. Want to edit your code in your specialized fork of microEMACS that seemingly doesn’t have undo in the container environment? Add it in the Dockerfile.
Want an LLM agent like Claude Code or Codex installed inside the container so it can’t do any damage outside the repository? Add it in the Dockerfile
Have a special private fork of some open source project which is deployed or built a little different? Just adjust the Dockerfile or container.sh file, depending on what you’re after.
This is also a less exotic setup than Nix-based dev environments. While many projects are very Nix-friendly, the ones that aren’t are really off the happy path of Nix in my experience, and will require some time to create a basic flake that has all the dependencies for the build to be successful. This still doesn’t get you isolation from your home directory, though!
The Docker-based approach trades off a little bit of convenience for a lot of isolation and reproducibility, while minimizing the amount of “exotic” in the setup to introduce unforeseen issues.
This is where autodevenv
comes in.
This is a tool that can create a similar structure for any repo you throw at it. It will create a wrapper repo, clone the target repo, and use a large language model of your choice (with your API key, model of choice, and API endpoint) that will attempt to read docs, the various types of lock files, README etc and generate basic instructions that you and another LLM can also read and follow along with to understand how the project works.
It will usually build a Debian or Ubuntu based image with the
dependencies installed inside of it, and (from the build instructions
doc) also generates a container.sh file that allows you to
build/run/test/run a shell/build the Docker image.
The tool is written in Rust, has minimal dependencies, and attempts to be pretty stateless. After the initial LLM run, you pretty much don’t need it for that repo.
It will have set up enough stuff up that any corrections or enhancements can be done by hand with very little specialized knowledge of the tool itself, or even by any LLM agent of your choice. The main advantage of the tool is in defining a structure, and the wrapper repo to keep track of your changes over time.
In the future, I intend for it to do what I currently have
just do in the initial two wrapper repos I created: allow
me to run the container commands no matter where in the target repo I
am, as well as allow adding custom instructions so your Dockerfile can
contain whatever programs you want right from the start.
It operates on whatever LLM you want: I was testing it out on Gemini 3 Flash, and got good results after some tweaking of the prompts.
Try it out today to keep your doors locked from amateur thieves!
This website was made using Markdown, Pandoc, and a custom program to automatically add headers and footers (including this one) to any document that’s published here.
Copyright © 2025 Saksham Mittal. All rights reserved. Unless otherwise stated, all content on this website is licensed under the CC BY-SA 4.0 International License