So, let's be honest. Do you trust your AI coder with full access to your machine? I sure didn't. After reading one too many horror stories about LLMs wiping disks, dropping databases, or deleting the wrong directory, I got a little paranoid. I needed to set some firm boundaries. I wanted to give my AI agents the autonomy to be useful, but without giving them the keys to the kingdom. This is the story of how I solved that problem using a pretty interesting tool: Nix.
My Goal: A Secure Playground with Very High Fences
I needed to create a sandbox for any coding agent, but it had to meet a few very strict rules. Think of it as a wishlist for peace of mind.
- Zero Trust. Period. The agent gets access to nothing by default. Not my home directory, not my SSH keys, or any other sensitive file.
- An Exclusive Toolkit. The sandbox should only contain the tools I explicitly approve. If the agent needs
gitandcurlto do its job, then that's all it gets. It shouldn't even know the rest of my systemPATHexists. - Controlled Access. Okay, it obviously needs network access to function. And it has to be able to read and write to the current project directory. But that’s it. A brick wall everywhere else.
Why Docker and Bubblewrap Didn't Cut It
My first thought was Docker. It’s the standard for containers, right? But I hit a wall with it almost immediately. It just felt… heavy for what I was trying to do. Worse, my entire development environment is already managed with Nix Flakes. Using Docker meant I'd have to basically copy that whole setup inside a Dockerfile. It felt clumsy and repetitive. Why manage my environment in two different places?
So I kept digging. My research eventually led me to bubblewrap, a fantastic lightweight utility for creating unprivileged sandboxes. It’s the same tech that powers Flatpak, so I knew it was solid. But it's also very low-level. How could I make it work smoothly with my Nix projects without writing a mountain of shell scripts?
Figuring out the answer to that question was how I found a NixCon talk by Alex David about jail.nix. It was perfect. It's a Nix-native library that gives you beautiful, high-level tools for building bubblewrap sandboxes. It understands Nix packages right away and lets me write my security rules declaratively. It was the elegant solution I was looking for.
If you're interested in diving deeper, Alex David has documentation and the source code available online.
The Flake: Let's See How It Works
This is the flake.nix I ended up with. It looks a bit dense at first, but let me break it down.
{
# --- 1. Inputs: Just listing our dependencies ---
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
jail-nix.url = "sourcehut:~alexdavid/jail.nix";
llm-agents.url = "github:numtide/llm-agents.nix";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { nixpkgs, jail-nix, llm-agents, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
jail = jail-nix.lib.init pkgs;
# I'm using crush and opencode, but you could swap in others.
crush-pkg = llm-agents.packages.${system}.crush;
opencode-pkg = llm-agents.packages.${system}.opencode;
# Common packages available to both agents
commonPkgs = with pkgs; [
bashInteractive
curl
wget
jq
git
which
ripgrep
gnugrep
gawkInteractive
ps
findutils
gzip
unzip
gnutar
diffutils
];
# Common sandbox options shared by both agents
commonJailOptions = with jail.combinators; [
network
time-zone
no-new-session
mount-cwd
];
# --- 2. The Sandboxes ---
makeJailedCrush = { extraPkgs ? [] }: jail "jailed-crush" crush-pkg (with jail.combinators; (
commonJailOptions ++ [
# Give it a safe spot for its own config and cache.
# This also lets it remember things between sessions.
(readwrite (noescape "~/.config/crush"))
(readwrite (noescape "~/.local/share/crush"))
(add-pkg-deps commonPkgs)
(add-pkg-deps extraPkgs)
]));
makeJailedOpencode = { extraPkgs ? [] }: jail "jailed-opencode" opencode-pkg (with jail.combinators; (
commonJailOptions ++ [
# Give it a safe spot for its own config and cache.
# This also lets it remember things between sessions.
(readwrite (noescape "~/.config/opencode"))
(readwrite (noescape "~/.local/share/opencode"))
(readwrite (noescape "~/.local/state/opencode"))
(add-pkg-deps commonPkgs)
(add-pkg-deps extraPkgs)
]));
in
{
lib = {
inherit makeJailedCrush;
inherit makeJailedOpencode;
};
# --- 3. Putting It All Together in the Dev Shell ---
devShells.default = pkgs.mkShell {
packages = [
pkgs.nixd # A little something for my editor.
(makeJailedCrush {})
(makeJailedOpencode {})
];
};
});
}
-
Inputs: This part is simple. We're just telling Nix where to find our dependencies: the standard Nix packages,
jail.nix, the agents themselves, and a helper library. -
Common Configuration: I've defined
commonPkgsandcommonJailOptionsonce and reused them for both agents. This keeps things DRY and makes it easy to add a new tool or security option to all agents at once. -
The Sandbox Definitions: Here's the core of it. We use
jail.nixto wrap our agents,crushandopencode. Thecombinatorsare the security rules, andmount-cwdis the bit that gives the agent access only to the project I'm working on. We also give each agent a couple of small directories for its own config so it can remember things. Theadd-pkg-depssection is my hand-picked list of commands the agents are allowed to use. They getgit,curl, andripgrep, but they have no clue that powerful tools likekubectl,gcloud, orssheven exist on my machine. -
The Dev Shell: Finally, I drop the newly created sandboxed agents into my development shell. Now, when I type
nix developin my terminal, the commandsjailed-crushandjailed-opencodeare sitting there, ready to go.
Real-World Example: Go Development
Let me show you a simpler, real-world example. Here's how I use jailed-agents for a Go project:
{
description = "development environment for go-grep-ast";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
jailed-agents.url = "path:/home/anderson/projects/jailed-agents";
};
outputs = { self, nixpkgs, jailed-agents }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
system = system;
config.allowUnfree = true;
};
in
{
devShells.${system}.default = pkgs.mkShell {
# https://nixos.wiki/wiki/Go#Using_cgo_on_NixOS
hardeningDisable = [ "fortify" ];
packages = with pkgs; [
go
gopls
golangci-lint
go-task
delve
gcc
(jailed-agents.lib.${system}.makeJailedCrush {
extraPkgs = [
go
gopls
golangci-lint
go-task
libgcc
gcc
];
})
];
};
};
}
You'll notice the Go packages are listed twice in the packages list. The first set (go, gopls, etc.) are available in your dev shell for you to use normally. The second set inside makeJailedCrush defines what the AI agent can access. This separation gives you full control. In that way, you can powerful tools in your environment while restricting what the agent uses.
The Result: Embracing 'YOLO Mode' Without the Fear
Honestly, using this setup is liberating. I can now fire up an agent on a complicated task, let it run wild in "yolo mode," and completely turn my attention elsewhere. I don't have a worry in the back of my mind that it might accidentally run a destructive command or wander into the wrong directory. Its world is strictly defined by the sandbox I built. It allows me to actually focus on my own work, checking in on the agent's progress when it's convenient for me.
What I really love is that I solved a very real problem using the tools I already enjoy. This isn't just some one-off hack, either. A few of my coworkers have already grabbed a version of this flake to lock down their own agents. It’s a small solution, but it solved a big problem for us.
Have you found yourself restricting your AI agents' access, or are you living on the edge? I'd love to hear how you're handling security in your development workflow.
Top comments (0)