08/03/2026

Sandboxing AI coding tools with Nix and Landlock

Sandboxing AI coding agents with Landlock and Nix

Kernel-level protection for your development environment

Most AI coding agents — Claude Code, GitHub Copilot CLI, Gemini CLI, OpenAI Codex, opencode — run with your full user permissions. They can read your SSH keys, API tokens, credential files, browser cookies, anything in your home directory. If an agent gets tricked by a prompt injection, it can exfiltrate all of it over the network.

This isn't theoretical. The Clinejection attack demonstrated exactly this: an attacker plants a malicious prompt in a PR description or a file in the repo. When the agent reads it, it follows injected instructions to steal credentials and send them to a remote server.

I built ai-cage to reduce this attack surface. It's a reusable Nix flake that uses Landlock with a strict default-deny policy.

How it works

Landlock is an unprivileged Linux Security Module (LSM) — no root required. It lets a process permanently reduce its own filesystem and network access. ai-cage uses landrun as the wrapper.

When you run an agent inside ai-cage, it gets:

  • A private HOME directory. Your real home stays hidden.
  • SSH agent forwarding without key file access. The agent can use your SSH agent socket, but cannot read ~/.ssh/id_*.
  • Restricted network. Only explicitly allowed TCP ports (443 and 22 in aiAgent).
  • Nix store execute allowlist. Only package closures you list can execute.
  • Inherited, irreversible restrictions. Child processes cannot escape the cage.

Why Landlock instead of containers?

Containers (Docker, Podman, bubblewrap) isolate via mount namespaces. Landlock keeps the same host filesystem/session and denies disallowed access paths. For AI-assisted local dev, this avoids a lot of friction.

No UID remapping surprises. Files created by the agent stay owned by your user, so Git and editor tooling keep working.

Simpler SSH usage. No socket bind-mount gymnastics across namespaces; $SSH_AUTH_SOCK just works when forwarded.

Nix-friendly model. You can expose /nix/store as read-only and execute only approved package closures.

Tradeoff: Landlock is access control, not full environment virtualization. It does not provide PID/network/mount namespaces.

ai-cage vs other Nix jailing options

If you're already in the Nix ecosystem, there are multiple ways to sandbox AI tools. Here's the practical comparison I ended up with:

  • ai-cage (Landlock + landrun): best for day-to-day AI coding on your real workspace. Same UID, same filesystem, low friction, no root required.
  • jailed-agents (bubblewrap/jail.nix): stronger filesystem illusion via mount namespaces, but more operational friction (bind mounts, occasional ownership/watcher weirdness, more moving parts).
  • nix develop / pure shells only: reproducibility and dependency isolation are great, but this is not a security boundary by itself; processes still run with your user permissions.
  • NixOS containers / podman-nix setups: stronger isolation layers and cleaner network separation, but heavier workflow and more setup complexity for local editing loops.

For the specific threat model "AI can edit my repo but should not read my secrets," ai-cage is the best balance of security and usability I found.

Using it

Import github:rolfst/ai-cage into your flake and define a cage wrapper:

caged-agent = ai-cage.lib.cage { inherit pkgs; } {
  name = "claude";
  profile = "aiAgent";
  argv = [ "${pkgs.claude-code}/bin/claude" ];
  packages = with pkgs; [ bashInteractive coreutils git openssh curl ripgrep ];

  filesystem = {
    ro = [ "$ORIG_HOME/.gitconfig" ];
    rw = [ "$ORIG_HOME/.config/claude" ];
  };

  env.pass = [ "ANTHROPIC_API_KEY" ];
};

ai-cage ships three profiles (offline, aiAgent, devNet) and supports custom profiles.

Hard-earned lessons

  • /dev and /tmp are mandatory for real workloads. Restricting too aggressively breaks common tools. ai-cage now explicitly grants safe required device paths and /tmp access.
  • Home-directory sibling file visibility exists in Landlock path traversal. If you allow --ro $HOME/.gitconfig, sibling files in $HOME/ (like .bashrc) can become readable. Subdirectories like .ssh/ and .gnupg/ remain blocked unless explicitly granted.
  • Best practice: avoid grants directly in $HOME/; copy required config into the cage state dir or grant a narrow subdirectory path instead.

What it does NOT protect against

I want to be upfront about the limitations:

  • Port-only network rules. Landlock cannot filter by hostname/IP.
  • Env var exfiltration. If you pass secrets in env.pass and allow outbound network, a compromised agent can still transmit those values.
  • No UDP controls. Landlock network restrictions cover TCP only.
  • Linux-only. Landlock is a Linux kernel feature.
  • Additive permissions in one ruleset. You cannot mark one file read-only inside a read-write directory in the same layer.

The goal is practical blast-radius reduction, not perfect containment. A constrained agent is still far safer than a fully privileged shell.

Code: github.com/rolfst/ai-cage

1 comment:

Dino Hensen said...

Thanks for the article. I'm not looking into setting up landrun myself. I'm not a nix user. I've thought long about this and one of the things that bother me is that if we want AI agents to run our software with access to real services, we need to inject real secrets/api-keys at runtime. I don't want to allow claude/codex to read a .env anymore, but hiding it just makes it worse, because the AI will now just add print statements to read the value of your secrets. In a professional setting we already know how to deal with this: have secrets in a production secrets store, give CI/CD runner privileges, now a production pipeline can run some script, and all of us devs can not. (maybe there is a break glass procedure to bypass in case of P1) We could have the software talk to mock services, or have local proxies inject secrets, but it all will make the experience of giving the AI permission to do it worse. I want to be able to walk away and let the AI do it's thing. For now I'm just going to setup landlock, make blastradius as small as possible and write another program that retroactively checks if claude or codex actually read a specific file and see how to deal with it. I'm already creating DEV keys for all services and set spend limits etc, but it's a lot of work. It's all part of the SDLC I guess... same as onboarding a new colleague I guess.