DEV Community

Cover image for How I Run LLM Agents in a Secure Nix Sandbox
Anderson. J
Anderson. J

Posted on

How I Run LLM Agents in a Secure Nix Sandbox

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.

  1. Zero Trust. Period. The agent gets access to nothing by default. Not my home directory, not my SSH keys, or any other sensitive file.
  2. An Exclusive Toolkit. The sandbox should only contain the tools I explicitly approve. If the agent needs git and curl to do its job, then that's all it gets. It shouldn't even know the rest of my system PATH exists.
  3. 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 {})
      ];
    };
  });
}
Enter fullscreen mode Exit fullscreen mode
  1. 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.
  2. Common Configuration: I've defined commonPkgs and commonJailOptions once 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.
  3. The Sandbox Definitions: Here's the core of it. We use jail.nix to wrap our agents, crush and opencode. The combinators are the security rules, and mount-cwd is 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. The add-pkg-deps section is my hand-picked list of commands the agents are allowed to use. They get git, curl, and ripgrep, but they have no clue that powerful tools like kubectl, gcloud, or ssh even exist on my machine.
  4. The Dev Shell: Finally, I drop the newly created sandboxed agents into my development shell. Now, when I type nix develop in my terminal, the commands jailed-crush and jailed-opencode are 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
          ];
        })
      ];
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

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)