Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
cplt — Sandbox for AI coding agents. Runs Copilot CLI, Claude Code, OpenCode, Gemini CLI, Antigravity, Pi, goose or a plain shell inside a kernel-level sandbox, with git and gh guards and sandbox policy committed to the repository. | Kitploit
Tools/GitHubGitHub/navikt/cplt
Defensive ToolsDynamic Analysis (Sandboxing)Configuration AuditingSecurity VirtualizationDevSecOpsCommand and ControlUtilities & FrameworksSecret DetectionSupply Chain SecurityAI Security
GitHub
12123861 day agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
navikt/cplt

cplt

Sandbox for AI coding agents. Runs Copilot CLI, Claude Code, OpenCode, Gemini CLI, Antigravity, Pi, goose or a plain shell inside a kernel-level sandbox, with git and gh guards and sandbox policy committed to the repository.

View RepositoryWebsite
Share

cplt

CI Release License: MIT macOS Linux

Kernel-enforced sandbox for AI coding agents. cplt wraps GitHub Copilot CLI, OpenCode, Gemini CLI, Antigravity CLI, Pi, Claude Code, goose, DeepSeek Harness, or any shell, so the agent can write code but cannot steal credentials, push to main, merge PRs, or exfiltrate secrets.

  • macOS: Apple Seatbelt/SBPL via sandbox-exec
  • Linux: Landlock LSM + seccomp-BPF + optional Bubblewrap namespace isolation (kernel 5.13+, full network filtering on 6.7+)
  • Windows: no native support. There is no Windows sandbox backend. Run cplt inside WSL2, where it is an ordinary Linux install and the Microsoft kernel ships Landlock. See Windows (WSL2) setup.

cplt banner

Why cplt?

AI agents execute arbitrary code. A compromised agent, whether through prompt injection, a supply chain attack, or a malicious MCP server, can read ~/.ssh, push to main, merge PRs, or exfiltrate your code, unless the OS itself says no.

cplt gives you kernel-level enforcement with team-configurable policy:

  • Per-repo policy in .cplt.toml, committed to version control, so it is tamper-proof and auditable
  • Deny by default for credentials, secrets, and sensitive files
  • Command-level git and gh interception: pushes to the default branch, force pushes, merges, and releases are blocked, feature branches stay open
  • Outbound network filtering with an audit log
  • No Docker, no VMs. One binary that runs on a locked-down laptop
  • Zero-config start for developers, with escape hatches when a build genuinely needs one

Table of contents

  • Quick start
  • What it blocks
  • How cplt compares
  • Install
  • Usage
  • Configuration
  • Architecture
  • Security
  • Network and proxy
  • Command guards
  • Known impacts
  • Limitations
  • Contributing
  • References

Detailed docs: Configuration · Proxy & domain filtering · gh command guard · git command guard · Known impacts · Security details · Security model

Quick start

root@kitploit:~
brew install navikt/tap/cplt   # macOS. On Debian or Ubuntu, see apt below
cplt --shell-install        # make 'copilot' run sandboxed (persistent)
                            # --agent opencode for any other agent
cplt doctor                 # check your environment
cplt -- -p "fix the tests"  # run Copilot in sandbox

Other agents and sandbox commands:

root@kitploit:~
cplt --agent opencode                       # OpenCode (Copilot subscription)
cplt --agent opencode --pass-env ANTHROPIC_API_KEY  # third-party provider
cplt --agent shell                          # interactive sandboxed shell (no AI)
cplt exec -- npm install                    # sandbox any command directly
cplt exec -c "npm install && npm test"      # compound commands in sandbox
alias npm="cplt exec -- npm"               # sandboxed npm for every invocation

Team rollout

root@kitploit:~
# 1. Generate per-repo policy
cplt init --write

# 2. Developers approve on first run
cplt trust accept --all

# 3. Tune the command guards (both block by default)
cplt config set git_guard.protect_default_branch_only false # block every push, not just main
cplt config set git_guard.mode warn                         # observe instead of blocking

What it blocks

The sandbox blocks access to credentials and secrets in the kernel. Command guards block destructive operations. Every restriction applies to the agent and to every process it spawns.

ResourceStatusNotes
Read/write project directory✅ Allowed
Read/write/delete .env*, .pem, .key in project🔒 Kernel-blockedPrevents secret exfiltration and destruction. --allow-env-files overrides
Write .git/hooks, .git/config, .gitmodules🔒 Kernel-blocked (macOS), ⚠️ partial on LinuxPrevents persistence via git hooks, hooksPath redirect, submodule hijacking. Linux: Landlock cannot deny a subpath inside an allowed tree, so these stay writable on the Landlock-only path. bwrap re-binds .git/hooks read-only but deliberately leaves .git/config and .gitmodules writable, so core.hooksPath remains a persistence route, see Linux limitations. Applies to every writable root, the project and each allow.write grant, including a granted worktree or bare repo whose real hooks live outside <root>/.git
Execute from /tmp, /var/folders🔒 Kernel-blockedPrevents write-then-exec. The scratch dir redirects TMPDIR to a safe location, on by default
Write PATH-resolved bin/shim dirs (~/.bun/bin, ~/.deno/bin, $PNPM_HOME, mise shims/ and all of installs/)🔒 Kernel-blocked (macOS), ⚠️ mise partial on LinuxPrevents trojaning a binary your next unsandboxed command resolves through PATH. Same reason ~/.cargo/bin and ~/go/bin have always been read-only. Breaks bun install -g, deno install, pnpm add -g, mise install, mise upgrade, mise use -g inside cplt, deliberately, and a repo pinning an uninstalled toolchain no longer bootstraps. Project-local installs are unaffected. Linux: mise's two ride the bwrap read-only overlay; the rest hold natively. See

That table is a summary. The sandbox also allows access to system files (SSL certs, /etc/hosts), temp directories (read and write, no exec), and system tool paths (/usr/bin, /opt/homebrew). Run cplt --print-profile for the complete SBPL rules.

For the full security model, threat analysis, and test strategy, read SECURITY.md.

How cplt compares

Codex CLI's sandbox

AreacpltCodex CLI sandbox
Outbound network controlCONNECT proxy with domain allow/block listsNo domain-level filtering
Environment handlingAllowlist plus hardening env injectionMore basic pass-through model
Secret file protectionDeny patterns such as .env*, .pem, .key inside the repoPrimarily directory-scoped access
Repo policy.cplt.toml with an explicit trust/approval flowNo repo-level policy file
Agent supportCopilot, OpenCode, Gemini CLI, Antigravity CLI, Pi, Claude Code, goose, DeepSeek Harness, or shellCodex only

cplt is not stronger everywhere. Codex CLI has Linux namespace isolation today, and it already exposes explicit sandbox modes such as read-only and workspace-write. cplt does not yet have that mode matrix.

Docker-based sandboxes

AreacpltDocker-based sandbox
Startup timeRoughly instant for normal CLI useUsually slower container startup
Network controlPer-request outbound filtering via proxyUsually all-or-nothing network access
File controlsPer-path and per-pattern rulesPer-mount controls
Host requirementsSingle binaryDocker daemon required
Corporate laptop fitWorks where Docker is unavailable or restrictedOften blocked by local policy

Docker still gives you stronger isolation in some environments, especially if you want a fully separate filesystem and process namespace. cplt trades that for lighter setup and tighter integration with the machine you already develop on.

VS Code agent mode permissions

Tools such as VS Code agent mode rely mainly on UI permissions. cplt enforces its restrictions in the kernel, so the agent cannot talk its way around them with a prompt or a modified instruction. That matters most for CLI agents and credential exposure:

  • cplt works outside the IDE
  • env vars are filtered before the agent starts
  • sensitive files can be blocked even when they live inside the repo
  • the same restrictions apply to child processes

Claude Code's sandbox (Anthropic Sandbox Runtime)

Anthropic Sandbox Runtime (srt) is the sandboxing layer used by Claude Code. Same high-level approach as cplt, macOS Seatbelt plus kernel-level Linux enforcement plus an HTTP proxy, different implementation.

AreacpltAnthropic srt
Language / deliverySingle Rust binaryNode.js + npm package + external deps
Linux backendLandlock LSM (no deps, no namespaces)bubblewrap (container via user namespaces)
Environment filteringStrict allowlist + suffix-deny (_TOKEN, _SECRET)Inherits full parent env (secrets pass through)
Credential dir protection15+ dirs denied by defaultUser must configure manually
DNS rebinding protection✅ Post-DNS IP checked against private ranges❌ Not implemented
Network proxyHTTP CONNECT + domain allow/blockHTTP + SOCKS5 + experimental TLS MITM
SSH gitBlocked at kernel on macOS (agent socket denied); on Linux only SSH_AUTH_SOCK is withheldProxied via SOCKS5
Package manager scriptsBlocked by default (npm_config_ignore_scripts)Not blocked
Agent supportCopilot, OpenCode, Gemini, Antigravity, Pi, Claude Code, goose, DSH, ShellClaude Code
ConfigTOML (global + per-repo)JSON (global only) + --control-fd live updates
Library API❌ Binary only✅ Embeddable TypeScript library

cplt is more secure out of the box: env filtering, credential protection, DNS rebinding checks, lifecycle script blocking. srt is more flexible: SOCKS5, TLS inspection, per-request callbacks, library embedding. The Linux backend choice matters. bwrap needs workarounds on Ubuntu 24.04+ because of AppArmor userns restrictions, while Landlock requires kernel 5.13 or newer but has zero external dependencies.

GitHub Copilot CLI's own sandbox

Copilot CLI has shipped with a local sandbox since June 2026, included in the standard seat. It runs shell commands through Microsoft MXC with restricted filesystem, network and system access, on macOS, Linux and Windows. /sandbox enable turns it on.

If that covers you, use it. It costs nothing extra, and it runs on Windows, which cplt does not.

Two things it does not do.

Policy lives with the administrator, not the repository. Enterprises set sandbox policy through Intune or another MDM. Nothing sits next to the code, so a rule that matters for one repository cannot follow it to a contributor, to CI, or to a laptop the MDM does not manage. In cplt the policy is .cplt.toml in the repository. Reviewers see changes to it in the pull request, and the file can tighten a developer's own configuration but never loosen it.

It confines the process, not what the process does with credentials it holds. The /sandbox tabs cover the filesystem, the network and system capabilities, and inside a Git repository the agent is granted read and write on .git by default. A sandboxed agent still has your gh token and your push access. Pushing a branch, merging a pull request and deleting a repository are all well-formed API calls from an authorised client, and a filesystem or network rule has no opinion about them. cplt wraps git and gh instead. The agent commits, branches and rebases freely. gh pr merge, gh repo delete and gh release create are blocked by default. So is git push to main/master; feature-branch pushes still work, because protect_default_branch_only is on. Set it to false to block every push, or git_guard.mode = "warn" to only warn.

Running both is reasonable. MXC confines the process. The guards decide what the agent may do with the credentials it holds.

Honest gaps

  • macOS has the strongest file-level enforcement today. Linux coverage is improving but not identical.
  • cplt does not yet offer simple read-only / workspace-write / full-access policy presets.
  • If you want full container isolation, cplt is not trying to replace Docker.

Install

Homebrew (recommended)

root@kitploit:~
brew install navikt/tap/cplt

mise

root@kitploit:~
mise use -g 'github:navikt/cplt@<version>'

mise picks the right release asset for your platform and verifies its build provenance attestation.

Pin the version. Our version strings are not comparable semver — they carry leading zeros and two hyphens — so mise latest can resolve to an older release than the newest one (navikt/copilot#818).

apt (Debian/Ubuntu, recommended on Linux)

navikt/apt is a signed archive served over GitHub Pages, carrying cplt and nav-pilot for amd64 and arm64:

root@kitploit:~
curl -fsSL https://navikt.github.io/apt/keyring/navikt-archive-keyring.gpg \
  | sudo tee /usr/share/keyrings/navikt-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/navikt-archive-keyring.gpg] https://navikt.github.io/apt stable main" \
  | sudo tee /etc/apt/sources.list.d/navikt.list
sudo apt update && sudo apt install cplt

It is a plain apt repository mirroring our releases, not a distribution package with its own maintainer. Its publish job runs hourly and pulls the newest .deb from each tool's latest release, so a release cut minutes ago takes up to an hour to become installable that way.

The package puts the binary at /usr/bin/cplt, and upgrades ride sudo apt upgrade from then on. cplt update refuses to touch an apt install and points at sudo apt upgrade instead: replacing the binary behind dpkg's back would be undone by the next apt run.

Without the archive, the same .deb is a release asset:

root@kitploit:~
arch=$(dpkg --print-architecture)   # amd64 or arm64
gh release download --repo navikt/cplt --pattern "*_${arch}.deb"
sudo apt install ./cplt_*_"${arch}".deb

curl | bash

For distributions that are not Debian derivatives, and for CI:

root@kitploit:~
curl -fsSL https://raw.githubusercontent.com/navikt/cplt/main/install.sh | bash

Options:

root@kitploit:~
# Install a specific version
curl -fsSL ... | bash -s -- --version 2026.05.05-174753-75bae5b

# Install to a custom directory
curl -fsSL ... | bash -s -- --dir ~/.local/bin

# Skip Homebrew (force direct download)
curl -fsSL ... | bash -s -- --no-brew

Download from releases

Grab the latest build for your platform from GitHub Releases:

root@kitploit:~
# macOS, Apple Silicon (M1/M2/M3/M4)
curl -fsSL https://github.com/navikt/cplt/releases/latest/download/cplt-aarch64-apple-darwin.tar.gz | tar xz
sudo mv cplt /usr/local/bin/

# macOS, Intel
curl -fsSL https://github.com/navikt/cplt/releases/latest/download/cplt-x86_64-apple-darwin.tar.gz | tar xz
sudo mv cplt /usr/local/bin/

# Linux, x86_64
curl -fsSL https://github.com/navikt/cplt/releases/latest/download/cplt-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv cplt /usr/local/bin/

# Linux, ARM64
curl -fsSL https://github.com/navikt/cplt/releases/latest/download/cplt-aarch64-unknown-linux-gnu.tar.gz | tar xz
sudo mv cplt /usr/local/bin/

Every release binary carries a build provenance attestation. Verify it:

root@kitploit:~
gh attestation verify cplt -o navikt

Build from source

root@kitploit:~
git clone https://github.com/navikt/cplt.git && cd cplt
cargo build --release
sudo cp target/release/cplt /usr/local/bin/

Or with mise:

root@kitploit:~
mise run install

mise run install and manual builds put cplt in /usr/local/bin/cplt. If you also have the Homebrew build at /opt/homebrew/bin/cplt, put /usr/local/bin first in PATH so your development build wins:

root@kitploit:~
# Check which cplt is active
which cplt

# If it shows /opt/homebrew/bin/cplt, reorder your PATH:
export PATH="/usr/local/bin:$PATH"

Or just run /usr/local/bin/cplt explicitly and skip PATH resolution entirely.

Windows (WSL2)

cplt has no Windows sandbox backend. Enforcement is Apple Seatbelt on macOS and Landlock LSM on Linux, so there is nothing to run natively on Windows. The supported route is WSL2, where cplt is an ordinary Linux install and the sandbox is kernel-enforced. Every Microsoft kernel branch builds CONFIG_SECURITY_LANDLOCK=y and lists landlock first in CONFIG_LSM (config-wsl), shipped since kernel 5.15.57.1, and WSL's default kernel command line sets no lsm= override.

In PowerShell, once:

root@kitploit:~
wsl --install                    # WSL2 + the default distro (now Ubuntu 26.04 LTS), then reboot
wsl --install -d Ubuntu-24.04    # ...or pin an older release
wsl --update                     # keep the Microsoft kernel current, see the ABI note below

Everything below runs inside the distro (wsl, or the Ubuntu profile in Windows Terminal), not in PowerShell:

root@kitploit:~
# 1. Node. Copilot CLI requires Node 22+
#    Ubuntu 26.04 ships 22.x, so apt is enough:
sudo apt update && sudo apt install -y nodejs npm
#    Ubuntu 24.04 ships Node 18, too old. Use nvm, fnm, or NodeSource there instead.

# 2. GitHub CLI, and log in. Ubuntu's universe package works but lags
#    (2.45 on 24.04); add GitHub's apt repo if you want a current gh:
#    https://github.com/cli/cli/blob/trunk/docs/install_linux.md
sudo apt install -y gh
gh auth login

# 3. The agent, installed in the distro, never on the Windows side
npm install -g @github/copilot

# 4. cplt, from the apt archive. The default distro is Ubuntu, so this is
#    the same route as on any other Debian derivative.
curl -fsSL https://navikt.github.io/apt/keyring/navikt-archive-keyring.gpg \
  | sudo tee /usr/share/keyrings/navikt-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/navikt-archive-keyring.gpg] https://navikt.github.io/apt stable main" \
  | sudo tee /etc/apt/sources.list.d/navikt.list
sudo apt update && sudo apt install cplt

# 5. Check the result
cplt doctor

Do not install Copilot CLI on the Windows side. With interop on (the default), the Windows PATH is appended to the distro's, so a Windows-side npm install -g @github/copilot turns up inside the distro as /mnt/c/Users/<user>/AppData/Roaming/npm/copilot. That is a Windows install reached through interop. It cannot run in the Linux sandbox, and the npm shim execs a node that the distro will not have unless you installed one there too. The symptom used to be an unrelated runtime-extraction error. cplt now names the cause when it resolves an agent under /mnt/<drive>/ and it is running under WSL, and cplt doctor reports it as a failing check instead of passing (#188). WSL is detected from kernel-owned state, either /run/WSL or the kernel name in /proc/sys/kernel/osrelease and /proc/version, not from WSL_DISTRO_NAME, which is absent under sudo and in systemd units and which any process can set. On a plain Linux box /mnt/c is left alone. It is an ordinary mount point there.

That check has two limits, both deliberate. It keys on the default automount root, so if you have relocated it ([automount] root in /etc/wsl.conf) the Windows-side install is not recognised and you get the old, less helpful failure with the path in it. And turning interop off stops the Windows PATH from leaking in but does not unmount /mnt/c.

Kernel and Landlock ABI. Current WSL (2.7.x and later) ships Linux 6.18, which gives Landlock ABI 7 — everything cplt uses except the unix-socket connect() right, which needs ABI 9 (kernel 7.1). An install still on the 6.6 kernel line gets ABI 3: filesystem rules are enforced, but TCP port rules (ABI 4), ioctl restriction (ABI 5) and signal/abstract-socket scoping (ABI 6) are not available, and network filtering falls back to the CONNECT proxy. wsl --update moves you forward. cplt doctor prints the kernel version and the ABI it found, which is the check that matters on your machine.

Do not disable Landlock in .wslconfig. A [wsl2] kernelCommandLine with an lsm= list that omits landlock, or a custom [wsl2] kernel= built without CONFIG_SECURITY_LANDLOCK, removes the kernel enforcement cplt depends on, and cplt doctor will report Landlock as unavailable.

Keep the project in the Linux filesystem. Work in ~/src/... inside the distro rather than /mnt/c/Users/.... Microsoft's own guidance is that cross-OS file access is markedly slower, and /mnt/c is served over 9p by default as of WSL 2.9.x (virtiofs is opt-in via [wsl2] virtiofs=true). More to the point, we have not verified how Landlock enforces rules on that mount. The kernel documents no exclusion for network- or FUSE-backed filesystems, only pipes, sockets and nsfs, and Landlock's own test suite exercises 9p and FUSE, so we expect it to work. Nobody here has confirmed it. Treat a project under /mnt/c as unproven rather than supported.

Bubblewrap. Ubuntu 23.10+ blocks unprivileged user namespaces through kernel.apparmor_restrict_unprivileged_userns, which breaks bwrap. That sysctl comes from an Ubuntu kernel patch that is absent from the Microsoft kernel, so the optional Bubblewrap layer is expected to work on Ubuntu-under-WSL2. That is inference from the kernel source, not something we have run. If bwrap fails there, please say so in #189. cplt's own seccomp filter is a plain PR_SET_SECCOMP BPF program, which stacks on top of the filter WSL installs in every process.

Not yet verified on a real WSL2 install. Verified from source: Landlock is compiled in and first in CONFIG_LSM on the Microsoft kernel; the /mnt/<drive>/ detection, the WSL signals it uses, and their error text; that cplt doctor fails on such an agent and prints kernel + Landlock ABI; the 5.13+/6.7+ requirements; and that install.sh installs the Linux release binary. Still unverified by anyone here: how Landlock behaves on /mnt/c, whether Bubblewrap works under WSL2, the exact package versions your distro release ships, and the sequence above end to end. If you run it, please report what actually happened in #189.

Shell setup (recommended)

By default you get the sandbox by typing cplt. To make plain copilot run sandboxed too:

root@kitploit:~
cplt --shell-install

That detects your shell, appends the alias to your rc file, and prints what it did. Run it as many times as you like, it will not add duplicates.

--agent picks which command gets the alias, and every agent cplt can launch is available:

root@kitploit:~
cplt --shell-install --agent opencode   # 'opencode' runs sandboxed
cplt --shell-install --agent claude     # and 'claude', alongside the others

Each install adds to your rc file rather than replacing what is there, so you can sandbox as many agents as you use. Without --agent you get copilot, which is what the flag has always installed.

ShellFile modifiedWhat's added (for --agent opencode)
zsh (macOS default)~/.zshrceval "$(cplt --shell-setup --agent opencode)"
bash~/.bashrceval "$(cplt --shell-setup --agent opencode)"
fish~/.config/fish/conf.d/cplt.fishalias opencode 'cplt --agent opencode'

--agent antigravity installs aliases for both antigravity and agy, since either name starts the same agent.

Restart your shell or source the file to activate.

There is no alias for --agent shell: there is no shell binary to shadow. Type cplt --agent shell for a sandboxed shell, or cplt exec -- <command> for a single command.

Manual setup (alternative)

If you would rather not use --shell-install, add the line yourself:

root@kitploit:~
# zsh / bash
eval "$(cplt --shell-setup --agent opencode)"

# fish
alias opencode 'cplt --agent opencode'

Same pattern mise, direnv, and starship use.

Why each alias names its agent. alias opencode=cplt would not do what it looks like. Plain cplt picks its agent from --agent, then the config file, then whatever it finds in PATH — and PATH detection prefers copilot. Typing opencode would sandbox Copilot instead, with nothing on screen to say so. The alias passes --agent so the command you type is the agent you get.

Why an alias instead of a symlink? cplt and Copilot CLI install into the same Homebrew bin directory (/opt/homebrew/bin/), and only one file named copilot can live there, so a symlink would conflict. An alias sidesteps that. The real copilot binary stays in PATH where cplt can find and wrap it, and the alias redirects your command.

Note: cplt refuses to nest. If it detects that it is already running inside a sandbox (via the __CPLT_WRAPPED environment variable), it will not launch again. Read-only subcommands such as --print-profile and cplt doctor still work inside an existing sandbox.

Usage

root@kitploit:~
cplt [OPTIONS] [-- <AGENT_ARGS>...]

Everything after -- goes straight to the agent process (copilot, opencode, gemini, antigravity, pi, claude, goose, dsh, or shell).

Policy presets

A preset sets a baseline for the five main sandbox toggles with one flag instead of a list of them. Individual flags still win over the preset, so --preset permissive --no-allow-tmp-exec does what it says. Also settable as [sandbox] preset = "..." in config.

FlagWhat it does
--preset strictFull network lockdown. All five toggles off, plus gh_guard, git_guard, proxy.forced (forced-proxy egress) and proxy.default_allowlist (fail-closed domain allowlist) on. Escape hatch: --allow-all-domains disables just the allowlist
--preset standardThe current defaults. All five off, scratch dir stays on. Same as passing no preset
--preset permissiveTurns on allow_localhost_any, allow_tmp_exec, and allow_lifecycle_scripts
--preset full-trust⚠️ Dangerous. Turns on all five, adding allow_env_files and allow_docker

Full preset matrix and resolution order: docs/configuration.md.

File access

The project directory is the writable workspace, plus a narrow allowlist needed for auth, runtime, and tooling (see the table above). The kernel blocks everything else, SSH keys and cloud credentials included.

FlagWhat it does
-d, --project-dir <DIR>Which directory Copilot can work in. Defaults to the current git repo root
--allow-read <PATH>Let Copilot read files outside the project, read-only. Repeatable
--allow-write <PATH>Let Copilot read and write outside the project. Use carefully. Repeatable. The tree is writable but not executable — a tree that is both is a binary-drop path, so an allow.write over ~/.cargo also stops ~/.cargo/bin running. Use --allow-exec on a separate, non-overlapping tree when you need both
--allow-exec <PATH>⚠️ Dangerous. Let the agent execute binaries from a tree outside the default tool directories — a relocated Homebrew or toolchain prefix, say. Grants read and execute, never write. Repeatable. Refused for an unsafe root (/, /tmp, $HOME and its parents, the platform system directories) and for any tree that overlaps a writable one — the project directory, an --allow-write grant, a writable tool directory such as ~/.cache, a writable agent data directory (~/.claude, ~/.local/share/opencode, ~/.pi/agent and the like), the real .git of a worktree or bare repo, or a tree the backends make writable with no grant at all (/tmp and /dev/shm on Linux; /private/tmp and /private/var/folders on macOS): writable plus executable is a binary-drop path, and neither backend can subtract the write grant from the exec grant
--allow-socket <PATH>⚠️ Dangerous. Allow a Unix domain socket path, for example a custom LSP daemon or a database socket. Repeatable. Whatever is on the other end runs outside the sandbox, so pointing this at docker.sock or an agent socket is equivalent to --allow-docker, and the only guard is that --deny-path overlaps are rejected. On Linux it does nothing below kernel 7.1, since unix socket connects are not gated by Landlock before ABI v9 (see Linux limitations)

Environment variables

cplt sanitizes the child environment by default. Only safe variables pass through, and cloud credentials, database URLs, and package tokens are stripped. It also injects hardening variables that block npm/yarn/pnpm lifecycle scripts (postinstall hooks, the number one supply chain attack vector), disable git commit and tag signing (since ~/.ssh and ~/.gnupg are unreachable inside the sandbox), and opt out of developer tooling telemetry (DO_NOT_TRACK=1, NEXT_TELEMETRY_DISABLED=1, TURBO_TELEMETRY_DISABLED=1, CHECKPOINT_DISABLE=1, and others).

What passes through:

CategoryExamplesHow
Core systemHOME, USER, PATH, SHELL, TMPDIR, LANGExplicit allowlist
TerminalTERM, COLORTERM, TERM_PROGRAMExplicit allowlist
EditorEDITOR, VISUAL, PAGERExplicit allowlist
Auth tokensGH_TOKEN, GITHUB_TOKEN, COPILOT_GITHUB_TOKENPassed only if you already set them. The gh guard uses a one-time file instead
Copilot configCOPILOT_DEBUG, COPILOT_*Prefix allowlist
Language runtimesNODE_*, GOPATH, CARGO_HOME, JAVA_HOME, VIRTUAL_ENV, PYTHONPATHExplicit allowlist
Tool managersNVM_*, FNM_*, PYENV_*, MISE_*, SDKMAN_*, COREPACK_*, YARN_*Prefix allowlist
OpenTelemetryOTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES, OTEL_*Prefix allowlist (OTEL_EXPORTER_OTLP_HEADERS may carry opt-in auth)
XDG dirsXDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME, XDG_CACHE_HOMEExplicit allowlist

Prefix allowlist with secret-suffix protection. A variable matching an allowed prefix such as COPILOT_* or YARN_* still gets dropped if it ends in a secret-bearing suffix: _TOKEN, _AUTH, _SECRET, _SECRET_KEY, _KEY, _PASSWORD, or _CREDENTIALS. So COPILOT_DEBUG passes and COPILOT_API_KEY does not.

Always blocked: AWS_*, AZURE_*, NPM_TOKEN, DATABASE_URL, VAULT_TOKEN, SSH_AUTH_SOCK, Docker vars, CI tokens, and anything not in the allowlist.

FlagWhat it does
--pass-env <VAR>Pass one environment variable through to the agent. Repeatable
--inherit-env⚠️ Dangerous. Inherit the full parent environment. Strips only NO_COLOR, FORCE_COLOR, SSH_AUTH_SOCK, SSH_AGENT_PID. Debugging only

Sandbox toggles

FlagWhat it does
--allow-lifecycle-scriptsAllow npm/yarn/pnpm lifecycle scripts (postinstall hooks) to run. Blocked by default. Use when npm install needs them
--allow-gpg-signingAllow GPG commit and tag signing inside the sandbox. Grants read-only access to the public keyring and the GPG agent socket. Private keys stay denied. See GPG signing
--allow-jvm-attachAllow JVM Attach API unix sockets in /tmp. Needed for MockK inline mocking, Mockito inline agents, ByteBuddy. See JVM Attach API
--allow-msbuildAllow MSBuild worker-node unix sockets in /tmp. Needed for dotnet build. Does not enable the persistent MSBuild Server. See MSBuild worker-node IPC
--no-scratch-dirDisable the per-session scratch directory, which is on by default. TMPDIR will not be redirected
--scratch-dirExplicitly enable the per-session scratch directory. Already the default, so this is for overriding scratch_dir = false in config
--brief🧪 Experimental. Write the agent-facing sandbox brief to the scratch dir (CPLT_BRIEF.md). Off by default. Also sandbox.brief = true in config. Unstable, so it may change or be removed in a future release
--no-briefTurn the sandbox brief off for this run, overriding sandbox.brief = true in config. Also suppresses the AGENTS.md block, which is gated on the brief
--agents-md🧪 Experimental. With --brief, also write the managed cplt block into the project's AGENTS.md. Off by default. Also sandbox.agents_md = true in config. No effect without --brief. Unstable, so it may change or be removed in a future release

Supported runtimes

cplt auto-discovers installed tools and writes sandbox rules to match. Generally only directories that exist on disk get rules, so there are no phantom paths. On macOS, writable app directories are included when discovered even if they do not exist yet, so they can be created on first use. Linux cannot allow a write to a non-existent path, so creation has to happen outside the sandbox there.

RuntimeHome dirsEnv vars / prefixesDiscovery
Node.js.nvm, .local/share/fnm, .local/binNODE_*, NPM_*, NVM_*, FNM_*node
Rust.cargo, .rustupCARGO_HOME, RUSTUP_HOMEcargo
Gogo/bin, go/pkgGOPATH, GOROOT, GOCACHE, etc.go
Java/Kotlin (JVM).sdkman, .jenv, .gradle, .m2JAVA_HOME, JAVA_TOOL_OPTIONS, GRADLE_*, MAVEN_*, SDKMAN_*, JENV_*java, gradle
Kotlin Native.konannonenone
Python.pyenvVIRTUAL_ENV, PYTHONPATH, PYENV_ROOT, PYENV_*python3
Yarn Berry.yarnYARN_* (hardening overrides YARN_ENABLE_SCRIPTS)yarn
pnpmLibrary/pnpm, .local/share/pnpmPNPM_HOMEpnpm
CorepacknoneCOREPACK_*none
mise.local/share/mise,

Run cplt doctor to see whether cplt will work here for your agent, and cplt doctor --verbose for everything it detected on your machine.

Debugging

FlagWhat it does
--doctorDeprecated. Use the cplt doctor subcommand instead
--print-profilePrint the generated sandbox profile (SBPL) and exit
--show-denialsStream macOS sandbox denial logs in real time
--no-validateSkip the startup check that verifies sandbox restrictions are active
-y, --yesSkip the interactive confirmation prompt. The configuration summary still prints, for auditability. Required when stdin is not a TTY, so CI and scripts need it
-q, --quietSuppress the startup banner and non-essential messages. Errors and warnings still print. Also sandbox.quiet = true in config
--no-quietOverride sandbox.quiet = true and show the startup summary anyway
--no-auditSkip the post-session change report. cplt normally diffs the working tree against a baseline commit pinned before the run and lists what the session touched, flagging sensitive paths. -q suppresses it too
--init-configCreate a starter config file at ~/.config/cplt/config.toml and exit

Session flags

These translate into the agent's own session flags, so you do not need a -- separator.

FlagWhat it does
--resume[=SESSION]Resume a previous session. Bare --resume picks interactively, --resume=NAME picks by name or ID
--continueResume the most recent session in the current directory
--remoteEnable remote control, so you can monitor and steer the session from GitHub.com or mobile
--name SESSIONName the session so --resume=NAME can find it later

--continue and --resume are mapped for OpenCode, Antigravity, and Claude Code too:

cplt flagCopilotOpenCodeAntigravity (agy)Claude Code
--continue--continue--continue--continue--continue
--resume--resume--continue¹--continue¹--resume
--resume=ID--resume=ID--session ID--conversation ID--resume ID
--remote--remoteignoredignoredignored
--name NAME--name NAMEignoredignoredignored

¹ Neither OpenCode nor Antigravity has an interactive session picker, so a bare --resume means "continue last session". Claude Code has one, so it maps straight across.

--remote and --name are Copilot-only. Pi and shell mode get no translation at all, so all four flags are dropped for them. Auto-resume is a separate mechanism: when you invoke cplt with no pass-through args and no session flags, it appends --resume for you, and that applies to Copilot only.

Combine them with sandbox flags and -- pass-through args:

root@kitploit:~
cplt --resume=my-task                          # resume by name
cplt --remote --name my-task -- -p "fix tests" # remote + named + prompt

Agents

Pick one with --agent <name>, or make it the default with cplt config set sandbox.agent <name>. Copilot, OpenCode, and Antigravity are auto-detected from PATH in that order when you do not name one.

Agent--agent valueAuto-detectedAuth
GitHub Copilot CLIcopilotyes, priority 1GitHub token, from the Keychain or gh
OpenCodeopencodeyes, priority 2Copilot subscription via /connect, or --pass-env ANTHROPIC_API_KEY
Antigravity CLIantigravity, aliases agy and agiyes, priority 3Google OAuth in the browser
Pipino--pass-env ANTHROPIC_API_KEY and friends
Claude Codeclaude, aliases cc and claude-codenoSubscription OAuth in ~/.claude or the Keychain, CLAUDE_CODE_OAUTH_TOKEN (drops the Keychain grant), or --pass-env ANTHROPIC_API_KEY
DeepSeek Harnessdsh, aliases deepseek and deepseek-harnessno--pass-env DEEPSEEK_API_KEY, or $DSH_HOME/.env (~/.dsh/.env)
Your shellshellnonone
  • Pi, Claude Code, goose, and DeepSeek Harness are never auto-detected. pi and dsh are generic binary names that could collide with something else on your machine, and Claude Code has to be chosen on purpose.
  • Third-party API keys are opt-in. ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN and the Bedrock/Vertex routing vars (CLAUDE_CODE_USE_BEDROCK, AWS_BEARER_TOKEN_BEDROCK, CLAUDE_CODE_USE_VERTEX, ANTHROPIC_VERTEX_PROJECT_ID, GOOGLE_CLOUD_PROJECT) never pass through unless you name them with --pass-env.
  • Subscription auth needs no env var. OpenCode's /connect device flow stores its token in ~/.local/share/opencode/auth.json, and Claude Code's OAuth token lives in ~/.claude (.credentials.json on Linux) or the macOS Keychain. Both are reachable inside the sandbox, so cplt does not nag about a missing API key for either.
  • OAuth browser flows need --allow-browser when a sign-in prompt appears. That covers Antigravity; every other agent here uses a device flow that prints a code and a URL and needs no browser. The flag lets the agent launch any application outside the sandbox and cannot be narrowed to URLs, so turn it on for the sign-in and off again — see the flag table and docs/security.md.
  • Claude Code auto-update is disabled with DISABLE_AUTOUPDATER=1. Claude Code has no --no-auto-update flag, self-updating inside the sandbox is a persistence vector, and it would fail against read-only install paths anyway.
  • CLAUDE_CONFIG_DIR is honored. When it is set, cplt grants that directory instead of ~/.claude and passes the variable through, so a relocated config root keeps working.
  • OpenCode is an officially supported Copilot client, so your existing Copilot subscription works with /connect inside OpenCode.

Per-agent config dirs, Keychain use, exec permissions, and env isolation are in SECURITY.md.

goose support

cplt can sandbox goose, the open-source AI agent (binary goose). Verified against goose 1.48.0.

root@kitploit:~
# Run goose (must be explicit — not auto-detected)
cplt --agent goose

# goose is provider-agnostic — pass your provider's API key
cplt --agent goose --pass-env ANTHROPIC_API_KEY
cplt --agent goose --pass-env OPENAI_API_KEY

# Skip the keyring entirely: keep the key in the environment
GOOSE_DISABLE_KEYRING=1 cplt --agent goose --pass-env OPENAI_API_KEY --pass-env GOOSE_DISABLE_KEYRING

# Set goose as your default agent
cplt config set sandbox.agent goose

Security notes for goose:

  • Not auto-detected: select explicitly with --agent goose or set sandbox.agent = "goose" in config
  • Provider-agnostic: goose routes model traffic to a user-configured provider (Anthropic, OpenAI, Google, Databricks, OpenRouter, …). Common provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY, GOOGLE_API_KEY, DATABRICKS_HOST/DATABRICKS_TOKEN, GROQ_API_KEY, OPENROUTER_API_KEY, XAI_API_KEY, AWS_BEARER_TOKEN_BEDROCK) are recognized auth hints and must be passed via --pass-env. goose reads GOOGLE_API_KEY, not GEMINI_API_KEY. Any provider outside this subset still works: name its variable with --pass-env
  • No default domains: goose contacted no host of its own in an --observe-domains capture, so its built-in allowlist is the shared package-registry base only. Add your provider's domain via allowed_domains before enabling --default-allowlist
  • Keychain is granted, and you can avoid it: goose stores provider secrets in the macOS login Keychain by default, so cplt grants it — but that grant is broader than goose's own entry (#242). GOOSE_DISABLE_KEYRING=1 makes goose use a secrets.yaml in its config dir instead, and passing the key with --pass-env avoids stored secrets altogether. On Linux goose uses the D-Bus Secret Service, which the Keychain grant does not affect
  • Config dir is read-only: ~/.config/goose/config.yaml declares extensions: entries whose cmd goose spawns on every session start, so a writable config dir is a host-persistence vector. Normal sessions do not write it; /mode changes and persisted tool permissions do not survive a sandboxed run. Reconfigure with goose configure outside cplt
  • goose's data (~/.local/share/goose/) and state (~/.local/state/goose/) dirs are writable, with exec denied. goose uses these XDG paths on macOS too, and honours the XDG_* overrides there
  • --continue and bare --resume map to goose session --resume; --resume=ID to goose session --resume --session-id ID; --name X to goose session --name X. These are subcommand flags, so cplt injects the session subcommand with them. --remote is ignored (no goose equivalent)

DeepSeek Harness support

cplt can sandbox DeepSeek Harness (binary dsh), the plugin-oriented agent harness from DeepSeek. Upstream ships it as a developer preview and its own SAFETY.md says not to rely on its controls as the only boundary, which is the case cplt exists for.

root@kitploit:~
# Run DSH (must be explicit — not auto-detected)
cplt --agent dsh

# Pass the API key, or keep it in $DSH_HOME/.env
cplt --agent dsh --pass-env DEEPSEEK_API_KEY

# Set DSH as your default agent
cplt config set sandbox.agent dsh

Security notes for DSH:

  • Not auto-detected: select it with --agent dsh (aliases deepseek, deepseek-harness) or set sandbox.agent = "dsh". dsh is a short, generic command name that could belong to something else on your machine
  • Turn DSH's own sandbox off inside cplt: DSH wraps every shell and file tool call in its own process sandbox — Seatbelt on macOS, bwrap or Landlock on Linux. Neither nests inside cplt. macOS does not support nested sandbox-exec calls (the same limitation that makes cplt turn Gradle's inner sandbox off, see Limitations), and bwrap builds its namespace with unshare, which cplt's seccomp filter denies. cplt is the enforcing boundary either way, so pick DSH's shipped danger-full-access permission preset for sandboxed sessions. Leave the inner runner on and tool calls fail with a sandbox runner error rather than a task error
  • One home root, and cplt follows the override: DSH keeps sessions, settings, cache and profiles under $DSH_HOME (~/.dsh by default). DSH_HOME is on the env allowlist, so the child resolves the same root cplt grants. A value pointing at a system root or your home directory is refused before launch, the same veto CLAUDE_CONFIG_DIR goes through
  • Host-persistence guard: $DSH_HOME/cordis.patch.yml, the home-level overlay the Loader reads at boot, is write-denied. $DSH_HOME/profiles/ stays writable because DSH rewrites each profile's cordis.yml include-root on every boot, so a per-profile cordis.patch.yml and installed plugins are a documented residual — do profile and dsh plugin edits outside cplt, and always launch dsh through cplt so anything planted still runs sandboxed
  • Default domains: deepseek.com only. The shipped dsh-llm-deepseek adapter defaults to https://api.deepseek.com. Point DEEPSEEK_BASE_URL at a gateway and you have to add that gateway's domain via allowed_domains
  • Auth: pass the key with --pass-env DEEPSEEK_API_KEY, or keep it in $DSH_HOME/.env. A key saved through DSH's own models UI lands in $DSH_HOME/.credentials.yaml, inside the same writable root. The macOS Keychain is denied, so git push over HTTPS needs gh's token in hosts.yml or --pass-env GH_TOKEN

Shell mode

Run a plain sandboxed shell with no AI agent and the same restrictions. Handy for testing build tools, debugging sandbox issues, or just working carefully by hand.

root@kitploit:~
# Interactive sandboxed shell (uses $SHELL: fish, zsh, bash)
cplt --agent shell

# Inspect what's allowed without entering the shell
cplt --agent shell --print-profile

The same deny-by-default rules apply: filesystem isolation, network restrictions, env sanitization. Shell config directories (fish variables and history, zsh history) stay writable.

For a single command, cplt exec is cleaner than cplt --agent shell -- -c 'cmd'.

Exec mode

Run any command inside the sandbox without starting an agent. No startup banner, no confirmation prompt, so it suits scripts, pipes, and shell aliases.

root@kitploit:~
# Sandbox a single command
cplt exec -- npm install
cplt exec -- make build
cplt exec -- go test ./...

# Compound commands via $SHELL -c
cplt exec -c "npm install && npm test"

# Pass sandbox flags as usual
cplt exec --allow-lifecycle-scripts -- npm install
cplt exec --project-dir /path/to/repo -- make build
cplt exec --with-proxy -- curl https://example.com

# Shell aliases for sandboxed tools
alias npm="cplt exec -- npm"
alias node="cplt exec -- node"
alias python="cplt exec -- python"

Every top-level cplt flag applies: --project-dir, --allow-read, --deny-path, --with-proxy, --pass-env, and the rest. Add --no-quiet to see the full sandbox configuration summary before the command runs.

Examples

root@kitploit:~
# The common case: Copilot in the sandbox
cplt -- -p "fix the tests"

# Sessions
cplt --resume                                   # pick one interactively
cplt --resume=my-refactor                       # by name
cplt --continue                                 # most recent in this directory
cplt --remote --name my-task -- -p "fix tests"  # named remote session

# Check the environment before the first run
cplt doctor

# Let Copilot read a shared library directory
cplt --allow-read ~/shared-libs -- -p "use shared-libs"

# Block a path you don't want Copilot to see
cplt --deny-path ~/.config/gh -- -p "refactor auth"

# Extra outbound port, e.g. an external API
cplt --allow-port 8443 -- -p "test the API"

# Localhost for MCP servers or dev servers
cplt --allow-localhost 3000 --allow-localhost 8080 -- -p "use the MCP server"

# All of localhost, needed by Next.js/Turbopack and Vite builds
cplt --allow-localhost-any -- -p "fix the build"

# Pass specific env vars through
cplt --pass-env MY_CUSTOM_VAR --pass-env ANOTHER_VAR -- -p "run with custom config"

# Inherit the full environment (dangerous, debugging only)
cplt --inherit-env -- -p "debug the build"

# Network
cplt --no-proxy -- -p "fix the tests"                    # proxy is on by default
cplt --blocked-domains ./blocked-domains.txt -- -p "refactor"
cplt --allow-private-domain intern.nav.no -- -p "use mcp-onboarding"

# Non-interactive / CI (skip the confirmation prompt)
cplt --yes -- -p "fix the tests"

# Inspect and debug the sandbox itself
cplt --print-profile
cplt --show-denials -- -p "fix the tests"

Configuration

Configuration happens at two levels: global, for developer preferences, and per-repo, for team policy.

root@kitploit:~
# Browse and change settings interactively
cplt settings

# Set global preferences
cplt config set sandbox.quiet true
cplt config set proxy.blocked_domains "~/.config/cplt/blocked-domains.txt"
cplt config set git_guard.mode warn      # observe pushes instead of blocking them
cplt config set gh_guard.enabled false   # opt out of the gh guard entirely

# Set per-repo policy (committed to .cplt.toml)
cplt config set --repo sandbox.allow_jvm_attach true
cplt config set --repo deny.paths "~/secrets"

# Inspect
cplt config show      # effective config (file + defaults)
cplt config explain   # every key with its description

cplt settings is the interactive editor, with Effective, Global, and Repository views, search, staged changes, and an explicit confirmation before it saves anything security-sensitive. cplt config stays the stable non-interactive interface for scripts and CI. Repository proposals are still committed and approved separately with cplt trust. The editor never commits or auto-approves them.

Precedence runs CLI flags, then the global config file at ~/.config/cplt/config.toml, then built-in defaults. Per-repo config in .cplt.toml is a separate layer rather than a rung on that ladder: [deny] tightens unconditionally, and approved permissions are additive only, so a repo can enable a feature but can never switch off something set by a CLI flag or global config.

A .cplt.toml in the repository root carries team policy:

root@kitploit:~
[deny]                    # Applied automatically, no opt-in needed
paths = ["~/secrets", "~/.vault-token"]
env = ["VAULT_TOKEN", "DATABASE_URL"]

[propose]                 # Requires developer approval (cplt trust accept)
gh_guard = true
git_push_prevention = true
allow_jvm_attach = true
allow_docker = true

[propose.allow]
ports = [5432]
localhost = [3000]
socket = ["/var/run/docker.sock"]

cplt reads it from git HEAD, so the agent cannot tamper with its own policy mid-session, and trust approvals are pinned to the file's content. An uncommitted .cplt.toml grants nothing until it is committed, though its [deny] keys still apply. In CI and scripts, where nobody can answer a prompt, --accept-repo-config approves the committed file's proposals for that one run without persisting any trust. cplt init writes one for you by detecting the project's tooling:

root@kitploit:~
cplt init             # preview detected permissions
cplt init --write     # write .cplt.toml to disk
cplt init --quiet     # output only TOML (pipe-friendly)
cplt init --global    # generate a personal ~/.config/cplt/config.toml

It knows JVM (Gradle/Maven), Node.js, Docker, Python, Rust, Go, Playwright, Spring Boot, Ktor, TestContainers, Next.js, Vite, Flyway, Cypress, and environment secrets from .env.example. Dangerous permissions come out of the generator with a risk warning attached. --global looks at machine-level things instead: Playwright browsers, GPG signing, registry credentials, alternative agents.

Some keys are global-only and rejected from .cplt.toml because they are machine-specific or a local preference: sandbox.agent, sandbox.quiet, sandbox.yes, sandbox.validate, sandbox.scratch_dir, sandbox.pass_env, sandbox.inherit_env, sandbox.allow_cache_exec, sandbox.allow_cache_exec_any, proxy.enabled, proxy.port, proxy.log_file, proxy.log_level, proxy.blocked_domains, proxy.allowed_domains, and every [gh_guard] and [git_guard] key.

Full details, including the trust model, path expansion rules, and the complete config file reference: docs/configuration.md.

Architecture

root@kitploit:~
┌──────────────────────────────────┐
│  cplt (Rust binary)              │
│  ┌───────────┐  ┌─────────────┐  │
│  │ Policy    │  │ CONNECT     │  │
│  │ Generator │  │ Proxy       │  │
│  └─────┬─────┘  │ (optional)  │  │
│        │        └─────────────┘  │
│        ▼                         │
│  ┌─────────────┬────────────┐    │
│  │   macOS     │   Linux    │    │
│  │  Seatbelt   │  Landlock  │    │
│  │  sandbox-   │  + seccomp │    │
│  │  exec       │  pre_exec  │    │
│  └─────────────┴────────────┘    │
│        │                         │
│        ▼                         │
│  copilot (sandboxed)             │
│  ├── All child processes         │
│  ├── Cannot read ~/.ssh          │
│  ├── Network port-restricted     │
│  ├── SSH agent blocked           │
│  └── Filesystem = primary ctrl   │
└──────────────────────────────────┘

The security model is a deny-by-default filesystem with kernel enforcement. On macOS, and on Linux with kernel 6.7+ (Landlock ABI v4), the network is restricted to port 443 by default, with --allow-port for extras. On older Linux kernels the CONNECT proxy provides that restriction instead, which is why it is enabled by default. SSH agent access and localhost outbound are blocked in the kernel on macOS. On Linux neither is: port-based Landlock rules cannot tell localhost from a remote host, and unix socket connect() is not gated by Landlock below kernel 7.1, so apart from the sockets bubblewrap masks the withheld SSH_AUTH_SOCK is the only thing standing between the agent and your loaded keys. The profile generator discovers your environment (cplt doctor --verbose shows the same probe results) and emits rules only for tool directories that actually exist on disk. Fewer rules, tighter sandbox.

  • macOS: a Seatbelt/SBPL profile is generated and handed to sandbox-exec
  • Linux: Landlock LSM rules plus a seccomp-BPF filter, applied via pre_exec (kernel 5.13+, TCP port filtering on 6.7+)

Internals and module layout: docs/architecture.md. Threat model, defense layers, and honest gaps: SECURITY.md.

Security

One binary, minimal dependencies, no runtime services, no telemetry. Three defense layers, with clear boundaries between them:

LayerEnforcementBypassable?What it protects
1. Kernel sandboxmacOS Seatbelt / Linux Landlock+seccomp❌ NoFile access, exec, network ports
2. Network proxyCONNECT proxy, domain filtering❌ No (within the sandbox)Outbound connections, exfiltration
3. Command guardPATH-based wrapper scripts⚠️ Soft barrierPushes, merges, releases, API writes

What cplt protects against:

  • Secret exfiltration (SSH keys, cloud credentials, .env files): kernel-blocked
  • Unauthorized code execution from temp dirs: kernel-blocked
  • Persistence via cache-dir binaries: kernel-blocked, since those dirs are denied exec
  • Persistence via git hooks in the project: .git/hooks is write-denied at the kernel on macOS. On Linux, with Landlock and no Bubblewrap, it stays writable, and cplt's own parent-side git then runs with core.hooksPath=/dev/null so it never executes a planted hook, though a git you run yourself still will
  • Persistence via package-manager tool dirs that are both writable and executable (mise shims, PNPM_HOME, ~/.deno/bin, ~/.bun/bin): write is granted there so pnpm add -g and friends work in-sandbox, so an agent can leave a binary behind that a later shell picks up off your PATH
  • A planted binary hijacking the agent cplt launches: at launch and audit, cplt resolves the helpers it runs itself (git, bwrap, sandbox-exec, mise, and the gh it reads a token from) from fixed system directories rather than PATH, but the agent binary itself runs from wherever it was discovered, which for an npm-global install is commonly under a writable mise or node tree. cplt cannot resolve it from a fixed directory — it legitimately lives where your version manager put it — so it checks the resolved path against the write rules the sandbox is about to apply and warns at launch, naming the binary and the writable tree, then proceeds
  • cplt doctor: its --version probes run each agent binary it finds on your PATH, in the parent, so a planted one executes there — the same discovered-path exposure as the launch above, which is why doctor is a report and not a boundary. Its gh check is resolved from the trusted directories and its kernel-release read spawns nothing at all
  • Data exfiltration to unauthorized domains: proxy-blocked
  • Accidental pushes to main and PR merges without review: guard-blocked

What cplt does not protect against:

  • Malicious code already inside the project directory. The agent has full read/write there
  • Logic bugs the agent introduces. You still review the code
  • A sophisticated adversary bypassing the command guard. Use server-side branch protection
  • Network attacks on allowed domains. If github.com is allowed, the agent can read and write there
  • macOS Keychain access, for agents that store auth there. Contents are password-protected, and sandbox.keychain_substitute can trade the grant away where an agent has another credential

Our priorities, in order: correct (every claim is tested, every edge case has a CVE or research reference), transparent (SECURITY.md hides nothing), simple (one binary, zero config required, sane defaults), and useful (get out of the way and let the agent work, safely).

More: docs/security.md · SECURITY.md

Network and proxy

The proxy is on by default. All outbound traffic from Copilot CLI, gh, and curl goes through a localhost CONNECT proxy via HTTP_PROXY/HTTPS_PROXY and NODE_USE_ENV_PROXY=1. It listens on an OS-assigned ephemeral port, so nothing collides. You get connection logging in real time, domain blocking, domain allowlisting, a persistent audit log, and the same port policy the sandbox enforces (443 plus anything in allow.ports).

root@kitploit:~
cplt --proxy-forced -- -p "fix tests"                 # force all egress through the proxy
cplt --no-proxy -- -p "fix tests"                     # disable for one run
cplt --blocked-domains blocked-domains.txt -- -p "x"  # block known-bad domains
cplt --allowed-domains allowed-domains.txt -- -p "x"  # allowlist mode
cplt --default-allowlist -- -p "x"                    # fail-closed: only the agent's own domains
cplt --observe-domains -- -p "x"                      # record what the agent contacts, block nothing
cplt --proxy-upstream http://proxy.corp:8080 -- -p "x" # chain through a corporate proxy

--observe-domains-out <FILE> writes the observed set one domain per line, and --proxy-upstream-no-proxy <HOST> lists hosts to reach directly instead of through the upstream.

root@kitploit:~
cplt config set proxy.enabled false
cplt config set proxy.blocked_domains "~/.config/cplt/blocked-domains.txt"
cplt config set proxy.allowed_domains "~/.config/cplt/allowed-domains.txt"
cplt config set proxy.log_file "~/.config/cplt/proxy.log"

Proxy-forced mode is opt-in. It restricts kernel egress to the proxy port so a socket opened directly, or an env -u HTTPS_PROXY, cannot slip past. Enforcement is full on macOS, which pins to localhost:<proxy_port>. On Linux it blocks direct TCP :443, and a seccomp rule permits only SOCK_STREAM with protocol 0 or IPPROTO_TCP for AF_INET/AF_INET6, so UDP, raw, SCTP and DCCP are closed too — at the cost of anything that opens such a socket, not only code that sends UDP. What remains is a port-based residual, evil.com:<proxy_port>, until #114.

Outside proxy-forced, Linux does not restrict UDP. Landlock's network rights are TCP-only until ABI v10, cplt handles AccessNet::ConnectTcp alone, and the seccomp rule above is deliberately not applied — denying SOCK_DGRAM there would break getaddrinfo(3), and so all DNS, for every non-proxied tool. Outbound UDP to any host, inbound UDP bind, DNS tunnelling and QUIC/HTTP-3 are therefore unmediated in default mode, and the CONNECT proxy carries TCP only, so none of it appears in the proxy log. macOS restricts UDP in default mode but does not route it either: remote ip "*:443" covers UDP, so QUIC/HTTP-3 on 443 leaves without touching the proxy there as well. Under proxy.forced the proxy log is a complete record of egress on macOS. On Linux it is complete except for the evil.com:<proxy_port> residual above, which does not traverse the proxy and so does not appear in its log.

Both lists match the same way: example.com covers the exact domain and every subdomain, matching is case-insensitive, and trailing dots are stripped. Blocklist and allowlist files are re-read every five seconds, so you can edit them live. Localhost traffic bypasses the proxy via NO_PROXY and never appears in the audit log. --proxy-timeout <SECONDS> bounds request and header reads (default 60) and does not tear down established CONNECT tunnels, which may idle for up to an hour.

Every proxy flag, domain-filtering detail, upstream corporate-proxy chaining, and the connection log format: docs/proxy.md.

Command guards

Enable them and cplt intercepts gh and git through wrapper scripts in $PATH:

CommandAction
gh pr merge, gh repo delete, gh release create🔒 Blocked
git push origin main, git push --force🔒 Blocked
gh api (write to other repos)🔒 Scope-checked
gh pr list, gh issue list, git commit✅ Allowed
git push origin feature-branch✅ Allowed with protect_default_branch_only

This is Layer 3, a soft barrier. It stops a compliant agent from doing something destructive by accident. For a hard boundary, lean on the kernel sandbox and server-side branch protection.

With the gh guard on, cplt also caches the GitHub token at launch and serves it once through the gh auth token callback, then deletes the cache. That cuts accidental and environment-based leakage. It is not a boundary against a hostile agent, because the cache lives in the agent's own TMPDIR and an agent that reads it before the legitimate consumer still gets the token. SECURITY.md has the full statement on block_auth_token.

Full behavior: docs/gh-guard.md · docs/git-guard.md

Known impacts

The sandbox blocks some workflows on purpose. The common ones and their fixes:

ImpactFix
.env files blockedcplt config set sandbox.allow_env_files true
npm postinstall hooks blockedcplt config set sandbox.allow_lifecycle_scripts true
go test / mise run blocked (temp exec)The scratch dir is on by default. If you still need it, cplt config set sandbox.allow_tmp_exec true
Localhost connections blockedcplt config set allow.localhost 3000, or cplt config set sandbox.allow_localhost_any true
Docker blockedcplt config set sandbox.allow_docker true ⚠️
SSH blockedUse HTTPS remotes instead
GPG signing disabledcplt config set sandbox.allow_gpg_signing true
JVM MockK/Mockito failscplt config set sandbox.allow_jvm_attach true
dotnet build MSBuild worker nodes blockedcplt config set sandbox.allow_msbuild true
Private registry creds blockedcplt config set allow.read "~/.m2/settings.xml"
Internal Maven/Nexus repo unreachable (Gradle/Maven)cplt config set proxy.allow_private_domains "intern.example.com". An IP-literal repository URL cannot be allowed — give the host a DNS name; see below
Playwright Chromium won't launchAllow cache exec, then disable Chromium's nested sandbox; see below

Playwright Chromium needs cplt config set sandbox.allow_cache_exec ms-playwright, and Chromium must run without its own nested sandbox. On macOS its helpers cannot initialize a second Seatbelt sandbox inside cplt (forbidden-sandbox-reinit); on Linux cplt's seccomp filter blocks the namespace syscalls that sandbox needs. Playwright as a library already launches with --no-sandbox, and that same opt-in sets PLAYWRIGHT_MCP_SANDBOX=false for Playwright MCP, which would otherwise turn it back on. Any other Chromium launcher needs --no-sandbox itself. cplt remains the enforcing kernel boundary, but a compromised renderer then receives the full cplt Playwright profile instead of Chromium's narrower child profile. See Cache exec and SECURITY.md.

Git commit works for every agent; whether git push works over HTTPS depends on the agent. Three prerequisites: use HTTPS remotes rather than SSH (git remote set-url origin https://github.com/org/repo.git, or rewrite globally with git config --global url."https://github.com/".insteadOf "[email protected]:"), run gh auth login once outside the sandbox, and run gh auth setup-git if the credential helper is not configured yet. Push then runs gh auth git-credential, which needs a token gh can reach from inside the sandbox — that differs per agent, see Git workflow. Pushes to the default branch and all force pushes are refused by the git guard by default; push a feature branch. The SSH agent socket is blocked because it unlocks every loaded key and can authenticate to any host, while the gh credential helper is scoped to GitHub.

The JVM is proxy-aware, so an internal Maven repository on a private IP now needs allowing. cplt injects http(s).proxyHost/proxyPort into JAVA_TOOL_OPTIONS, so Gradle and Maven dependency resolution goes through the CONNECT proxy and shows up in the proxy log instead of bypassing it. The proxy's SSRF guard then refuses an in-house Nexus or Artifactory that resolves into private address space, exactly as it already does for curl, npm and pip. Add its DNS name to proxy.allow_private_domains. A repository URL written as a bare IP literal (https://10.20.30.40/repository/maven-public/) cannot be allowed by any key — that check runs before the allow list is consulted — so such a repository needs a DNS name. WorkerExecutor plugin forks, and a Gradle daemon started outside cplt and reused inside, are not proxied. See Internal Maven/Gradle repositories on private IPs.

Gradle 9+ runs its own nested sandbox, and cplt turns it off. Since Gradle 8.8 the daemon wraps itself in sandbox-exec (controlled by GRADLE_MACOS_SANDBOX, previously the org.gradle.daemon.sandbox property). macOS does not support nested sandbox-exec calls, so the inner sandbox fails with "Operation not permitted" on socket operations. cplt injects GRADLE_MACOS_SANDBOX=off, since it already provides kernel-level sandboxing. This is a known upstream issue that hits any tool wrapping Gradle in an outer sandbox. Override with --pass-env GRADLE_MACOS_SANDBOX if you really want Gradle's own sandbox.

Copilot CLI 1.0.83 runs its own nested sandbox, and cplt turns it off. On Linux that sandbox builds a network namespace — slirp4netns, iptables, /dev/net/tun — and cplt's seccomp filter denies the unshare it takes. cplt also sets HTTP_PROXY/HTTPS_PROXY, which in 1.0.83 puts a Linux sandbox on the proxy-egress path whether or not you asked for it, so the two collide on every launch. Symptom: [cplt] Starting Copilot in sandbox... and then nothing. cplt injects Copilot's own opt-out, COPILOT_CLI_SANDBOX_SUPPORT_OVERRIDE=unsupported; Copilot stands down for the session, says so, and leaves your saved sandbox.enabled alone. cplt is the boundary, as it is for Gradle and Chromium. Override with --pass-env COPILOT_CLI_SANDBOX_SUPPORT_OVERRIDE. An enterprise managed policy that requires the sandbox overrides all of this — see Copilot CLI's own command sandbox.

Every impact, with the per-tool tables, JVM and Kotlin daemon notes, GPG troubleshooting, and the private-registry platform differences: docs/known-impacts.md.

Limitations

macOS

  • sandbox-exec is deprecated. Apple has not removed it, but may in a future macOS version.
  • SBPL has no domain-based filtering. The optional CONNECT proxy provides domain blocking instead.
  • SBPL's lsopen has no filter either, so --allow-browser is all of Launch Services or none of it. With it on the agent can launch any application outside the sandbox, and no wrapper can narrow that — see docs/security.md.

Linux

  • Kernel 5.13+ required, with Landlock LSM enabled.
  • TCP port filtering needs kernel 6.7+. Older kernels get filesystem-only enforcement.
  • Landlock cannot deny subpaths within allowed paths, so .env read/write/delete inside the project dir is not kernel-enforced. .git/hooks writes are blocked when Bubblewrap is active.
  • --deny-path requires Bubblewrap. It is enforced through mount masks when bwrap is active. Without it, Landlock is allowlist-only and cplt warns about the deny instead of applying it.

More: docs/security.md

Contributing

Contributions are welcome.

root@kitploit:~
git clone https://github.com/navikt/cplt.git && cd cplt
git config core.hooksPath hack    # enables pre-commit fmt + clippy checks
mise run check                    # runs fmt, clippy, and tests

Open an issue before starting a large change. Every PR has to pass CI (fmt, clippy, tests).

References

  • SECURITY.md, the full security model, threat analysis, test strategy, and prior art
  • Apple sandbox-exec(1)
  • Chromium Seatbelt V2 Design
  • Landlock LSM documentation
  • seccomp-BPF documentation
  • OWASP SSRF Prevention Cheat Sheet
  • michaelneale/agent-seatbelt-sandbox

License

MIT

Download Tool
Global tool installs
Execute from ~/Library/Caches🔒 Kernel-blocked by defaultPrevents binary-drop staging. Copilot native modules are exempted via a carve-out. Add targeted exemptions with --allow-cache-exec <SUBDIR>, e.g. ms-playwright
Modify .vscode/tasks.json, launch.json⚠️ Allowed, known riskIDE trust boundary. See SECURITY.md for mitigations
Read/write ~/.copilot (auth, settings)✅ AllowedIncludes file-map-executable for keytar.node, pty.node, computer.node
Write ~/.copilot/pkg (native modules)🔒 Kernel-blockedPrevents persistence via native module replacement
Environment variables🔒 Sanitized + hardenedOnly a safe allowlist passes through. Lifecycle scripts blocked. --pass-env VAR adds one back
Read ~/.config/gh/hosts.yml + config.yml✅ Allowed (read-only)Only these two files. The rest of .config/gh is blocked
Read ~/.config/mise✅ Allowed (read-only)Tool versions and PATH, no secrets
Read ~/.gitconfig, ~/.config/git/config✅ Allowed (read-only)A dotfiles symlink is followed to its target, so a stowed ~/.gitconfig works
Read ~/.git-credentials🔒 Kernel-blockedcredential.helper = store keeps cleartext tokens here. No --allow-read reopens it, like ~/.netrc. Linux: a grant on an ancestor ($HOME itself) still exposes it, because Landlock cannot deny a subpath inside an allowed tree
Read global git hooks (core.hooksPath)✅ Allowed (read-only, write-denied)Auto-detected. Must be under $HOME with depth ≥3. Writes are explicitly blocked
Commit/tag signing (commit.gpgsign, tag.gpgsign)🔒 DisabledPrivate keys in ~/.ssh and ~/.gnupg are blocked, so signing is disabled via an env var override
Read ~/Library/Application Support/Microsoft✅ Allowed (read-only)Device ID for telemetry
Access macOS Keychain⚠️ Allowed (read+write) for agents that store auth thereThe grant cannot be scoped to one item, so it reaches every keychain entry the agent can unlock. Opt in to sandbox.keychain_substitute (EXPERIMENTAL, default off) to drop it on runs where the agent can authenticate without it — CLAUDE_CODE_OAUTH_TOKEN for Claude Code, an existing fallback token file for Antigravity. See SECURITY.md
Outbound network (port 443)✅ AllowedEvery other port is blocked. Add extras with --allow-port
Localhost outbound🔒 Kernel-blocked (macOS), ⚠️ port-based on LinuxPrevents local service access. Inbound still works for the proxy. Linux: Landlock rules are port numbers only and cannot tell localhost:443 from remote:443, so a local service on an allowed port is reachable and there is no localhost-specific deny. Use --with-proxy for SSRF protection, see Linux limitations
SSH agent (unix socket)🔒 Kernel-blocked (macOS), ⚠️ env-only on LinuxPrevents signing git operations or SSH to hosts. Linux: unix socket connect() is not gated, so the withheld SSH_AUTH_SOCK is the only barrier and an agent that sets it itself can use the loaded keys. bwrap hides the stock OpenSSH socket under /tmp, but not a gnome-keyring/gcr or systemd agent under $XDG_RUNTIME_DIR. See Linux limitations
Developer tools (~/.cargo, ~/.gradle, ~/.m2, ~/.sdkman, ~/.jenv, ~/.pyenv, ~/.konan, etc.)✅ Allowed (read+write for caches)Only dirs that exist on disk. Tightened at runtime by what cplt doctor detects
Registry credential files (~/.m2/settings.xml, ~/.gradle/gradle.properties, ~/.cargo/credentials)🔒 Kernel-blocked on macOS. On Linux the parent tool dir stays readableOverride with --allow-read. See Private registries
Read ~/.npmrc🔒 Kernel-blocked (both platforms)Override with --allow-read. Breaks yarn 1, see yarn 1
Go source code (~/go/src)🔒 Kernel-blockedOnly ~/go/bin and ~/go/pkg are readable
Read ~/.ssh, ~/.gnupg, ~/.aws, ~/.azure🔒 Kernel-blocked
Read ~/.kube, ~/.docker, ~/.nais🔒 Kernel-blocked
Read ~/.password-store, ~/.terraform.d🔒 Kernel-blocked
Read ~/.config/gcloud, ~/.config/op🔒 Kernel-blockedIndividual files are overridable with --allow-read. See Cloud credentials
Read or write ~/.config/cplt, ~/.nav-pilot🔒 Kernel-blockedTool state that decides what the next launch may do. ~/.config/cplt is un-overridable as a whole subtree; inside ~/.nav-pilot, a named path stays grantable so a pinned agentpakke payload can be read
Read ~/.netrc, ~/.pypirc, ~/.vault-token🔒 Kernel-blockedUn-overridable on both platforms. Naming one in allow.read is a startup error
Read ~/.gem/credentials🔒 Kernel-blockedUn-overridable on both platforms. Naming one in allow.read is a startup error
gh CLI destructive operations (merge, delete, release)🔒 Command-gated (on by default)Opt out with --no-gh-guard. See gh guard
git push to the default branch🔒 Command-gated (on by default)Blocks pushes to main/master; feature-branch pushes still work. protect_default_branch_only = false blocks every push, git_guard.mode = "warn" only warns, --no-git-guard opts out
Child process inheritance✅ All restrictions apply to subprocesses
--deny-path <PATH>
Block a path that would otherwise be allowed. Deny always wins. Repeatable
--allow-port <PORT>Allow outbound traffic on an extra port. Only 443 by default. Repeatable. On macOS the rule is (remote ip "*:PORT"), which is family-agnostic and so carries UDP as well as TCP; Landlock gates TCP connect only. Under proxy.forced the port opens no direct socket at all — it is reachable through the proxy, so proxy-aware tools keep working
--allow-localhost <PORT>Allow outbound to localhost on one port. Localhost is blocked by default. Use for MCP servers or dev servers. Repeatable
--allow-localhost-anyAllow outbound to localhost on all ports. Needed by build tools like Turbopack (Next.js) and Vite that use random ephemeral ports for IPC
--no-agents-mdTurn the AGENTS.md block off for this run, overriding sandbox.agents_md = true in config. Leaves the scratch-dir brief alone
--allow-tmp-exec⚠️ Dangerous. Allow exec from system temp dirs (/private/tmp, /private/var/folders). Prefer the scratch dir
--allow-cache-exec <SUBDIR>Allow exec from one ~/Library/Caches/<SUBDIR>. Repeatable. For tools that cache compiled binaries there, such as Playwright and pnpm dlx
--allow-cache-exec-any⚠️ Dangerous. Allow exec from all of ~/Library/Caches. Prefer --allow-cache-exec <SUBDIR>
--allow-browser⚠️ Dangerous. With this on, the agent can launch any application on your machine outside the sandbox. The grant is Launch Services, not a browser: launchd starts the target outside the Seatbelt profile, so open -a Terminal /tmp/x.sh runs unsandboxed. This cannot be scoped to URLs — SBPL's lsopen takes no filter, and the grant is reachable through LSOpenCFURLRef() without the open binary at all, so no wrapper can narrow it (#251, and docs/security.md). Only turn it on while a sign-in prompt is actually on screen (MCP server OAuth, re-auth), then turn it back off. Off by default
--deny-clipboardBlock the agent from reading or writing the macOS clipboard (pbpaste/pbcopy) by denying the com.apple.pasteboard Mach service. Every other Mach service (Keychain, DNS, Security framework) is unaffected. On by default — this flag restates the default
--allow-clipboardGive the agent back the macOS clipboard, which cplt denies by default. Equivalent to sandbox.deny_clipboard = false
--use-bubblewrapLinux only. Require the bubblewrap namespace layer (PID, mount, IPC, UTS, cgroup, user namespaces plus a private /tmp) on top of Landlock and seccomp. Errors out if bwrap is missing. Auto-detected when neither flag is given
--no-bubblewrapLinux only. Never use bubblewrap, even when installed. Falls back to Landlock and seccomp. Use it when bwrap breaks a specific tool
.mise
MISE_*
mise