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
/devand/tmpare mandatory for real workloads. Restricting too aggressively breaks common tools. ai-cage now explicitly grants safe required device paths and/tmpaccess.- 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.passand 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.