National Cyber Warfare Foundation (NCWF)

Containing AI coding agents with cplt: kernel-level sandboxing for the agentic dev workflow


0 user ratings
2026-09-23 11:28:05
milo
Red Team (CNA)
"Containing

cplt wraps AI coding agents like Copilot CLI, Claude Code, and OpenCode in kernel-enforced sandboxes so authorized developers can run agents without exposing credentials or repositories.








Toolnavikt/cplt — kernel-enforced sandbox for AI coding agents, written in Rust under MIT
CategoryHost containment / sandboxing for developer tooling
Primary UseRunning agents such as Copilot CLI, OpenCode, Gemini CLI, Antigra or goose inside a kernel sandbox with per-repo .cplt.toml policy, git/gh command guards, and network filtering
Safe UseLegitimate defensive containment for developers and security teams running AI agents on their own workstations during authorized development, testing, and internal security reviews
Telemetry NotePurely defensive containment; cplt writes an audit log of outbound network attempts and emits cplt doctor diagnostics — nothing that touches third-party systems beyond the proxied agent traffic itself

cplt is a Rust binary from navikt (the Norwegian Labour and Welfare Administration's IT unit) that answers a question every team running AI coding agents now faces: what happens when the agent is tricked into doing something destructive? Rather than trusting the agent's own guardrails, cplt pushes enforcement down into the operating system kernel, wrapping tools like GitHub Copilot CLI, OpenCode, Gemini CLI, Antigra, Claude Code, goose, DeepSeek Harness, or even a plain shell so the agent can write code but cannot steal credentials, push to main, merge PRs, or exfiltrate secrets. The threat model is explicit in the README: prompt injection, supply chain attacks, and malicious MCP servers are all named as compromise vectors.


The platform story is a study in modern Linux and macOS containment primitives. On macOS, cplt uses Apple's Seatbelt policy language, SBPL, applied through sandbox-exec. On Linux, it layers Landlock LSM, seccomp-BPF, and optional Bubblewrap namespace isolation, with full network filtering requiring kernel 6.7+ and basic Landlock support from 5.13+. There is deliberately no native Windows backend; the README directs Windows users to WSL2, where the Microsoft kernel ships with Landlock support and cplt runs as an ordinary Linux install. No Docker, no VMs — one static binary on a locked-down laptop.


What elevates cplt above a personal wrapper script is its configuration model. Policy lives in a per-repo .cplt.toml committed to version control, which makes it tamper-resistant and auditable across a team. Developers generate the file with cplt init --write, approve it on first run via cplt trust accept --all, and the repository owner tunes behavior with commands like cplt config set git_guard.protect_default_branch_only false to block every push rather than just main. This is policy-as-code applied to agent containment, and it is the pattern that makes enterprise rollout plausible.


The default posture is deny-by-default for anything credential-shaped. The README's blocking table is remarkable in its granularity: ~/.ssh, ~/.gnupg, ~/.aws, ~/.azure, ~/.kube, ~/.docker, ~/.terraform.d, ~/.password-store, ~/.config/gcloud, and ~/.config/op are all kernel-blocked reads. Some files are flagged as un-overridable on both platforms — ~/.netrc, ~/.pypirc, ~/.vault-token, and ~/.gem/credentials cannot be reopened even with --allow-read, and naming one in an allow.read grant is a startup error. That design choice shows real operational maturity: the authors anticipated pressure to poke holes and refused the most dangerous ones.


The command guards are where cplt shows its git-centric thinking. A compromised agent's most attractive moves — pushing to the default branch, force pushing, merging PRs, cutting releases — are intercepted at the git and gh command level while feature branches remain fully usable. The guard is on by default, can be flipped into observe-only mode with git_guard.mode = "warn", or disabled per-invocation with --no-git-guard and --no-gh-guard. Pairing kernel-level file deny rules with semantic command interception is a sensible two-layer defense: the kernel stops raw file access while guards catch intent expressed through tooling.


Equally interesting is the anti-persistence work. cplt kernel-blocks writes to .git/hooks, .git/config, and .gitmodules on macOS (with documented partial coverage on the Landlock-only Linux path), preventing an agent from planting hooks that execute on your next unsandboxed commit. It blocks execution from /tmp and /var/folders to kill write-then-exec staging, redirects TMPDIR to a safe scratch directory, and blocks writes to PATH-resolved binary directories like ~/.bun/bin, ~/.deno/bin, and $PNPM_HOME so the agent cannot trojan a binary your next unsandboxed command resolves. The README is honest that this deliberately breaks bun install -g and mise install inside the sandbox — a documented tradeoff, not a bug.


The README is unusually candid about limitations, which is a strong trust signal. The Linux Landlock path cannot deny a subpath inside an allowed tree, so blocking .git/hooks inside an allowed project root is impossible without bwrap, and an ancestor grant on $HOME still exposes ~/.git-credentials. Landlock's network rules are port-based only, meaning localhost:443 is indistinguishable from a remote host on the same port — hence the recommendation to use --with-proxy for SSRF protection. Only port 443 is allowed outbound by default, with extras via --allow-port, and the macOS Keychain grant is acknowledged as all-or-nothing, with an experimental sandbox.keychain_substitute to drop it on runs where the agent can authenticate via CLAUDE_CODE_OAUTH_TOKEN instead.


Environment handling follows the same allowlist philosophy. Only a sanitized, hardened set of environment variables passes through to the agent, lifecycle scripts are blocked, and --pass-env VAR reintroduces one at a time — the README shows cplt --agent opencode --pass-env ANTHROPIC_API_KEY for third-party providers. Every restriction applies to the agent and every subprocess it spawns, closing the trivial escape of shelling out to an unconfined child. A cplt doctor command checks the environment and tightens runtime grants based on what actually exists on disk.


From a defensive engineering perspective, cplt is also a blueprint for thinking about agent containment generally. The idea that tool state deciding what the next launch may do — ~/.config/cplt and ~/.nav-pilot — is itself kernel-blocked against the sandboxed agent is a subtle self-protection measure against an agent rewriting its own warden. Known risks are documented rather than hidden: .vscode/tasks.json and launch.json remain writable, deferring to the IDE trust boundary, and SSH-over-443 reachability to ssh.github.com is called out explicitly.


Getting started is low-friction: brew install navikt/tap/cplt on macOS (with an apt path for Debian/Ubuntu), then cplt --shell-install to transparently sandbox the copilot command, and cplt -- -p "fix the tests" to run the agent. For non-AI use, cplt exec -- npm install sandboxes arbitrary commands, and an alias npm="cplt exec -- npm" pattern extends containment to everyday tooling. At roughly 121 stars, cplt is early in its lifecycle, but the depth of its threat modeling, the honesty of its limitation docs, and its MIT-licensed Rust implementation make it one of the more serious entries in the emerging agent-isolation space — worth studying even by teams building their own containment.



Official project repository for navikt/cplt.

Download Tool

Educational analysis for authorized security professionals. Use only in controlled, authorized environments.






Source: OffensiveSec
Source Link: https://www.offsecblog.com/2026/09/containing-ai-coding-agents-with-cplt.html


Comments
new comment
Nobody has commented yet. Will you be the first?
 
Forum
Red Team (CNA)



Copyright 2012 through 2026 - National Cyber Warfare Foundation - All rights reserved worldwide.