cplt
AGENTS.md140 starsChanged 18 days agoMITView on GitHub
Rust project. A kernel-enforced sandbox for AI coding agents, using macOS Seatbelt (SBPL) and Linux Landlock plus seccomp-BPF and optional bubblewrap, with an HTTP CONNECT proxy in front of outbound traffic. It wraps Copilot, OpenCode, Antigravity, Pi, Claude Code, and a plain shell. Always run mise run check at the end of a coding session. Both e2e suites spawn a fake agent binary from inside the project tree, and the sandbox denies exec under /tmp and /var/folders by design. A…
We split this AGENTS.md into the rules it gives an agent
17 rules. About 0 are common, 8 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 10
Always run mise run check at the end of a coding session.
- Distinctive: line 32
Use their targeted tasks while iterating on those boundaries.
- Very specific: line 57
Prefer &str/&[T] over owned types in function signatures
- Distinctive: line 59
Only comment code that needs clarification
- Distinctive: line 106
Unit tests: verify profile and policy generation: the SBPL profile text and Landlock policy contain the correct allow/deny rules, env vars …
- Very specific: line 110
Integration tests: verify kernel enforcement by running a real command inside the sandbox (macOS sandbox-exec, Linux Landlock+seccomp) and …
- Distinctive: line 113
E2E tests: verify the full binary pipeline: CLI arg parsing → profile generation → sandbox invocation → exit code.
- Distinctive: line 115
E2E project tests: verify that realistic developer workflows (git, node, python, rust file ops, config files) work inside the sandbox.
- Distinctive: line 152
Not weaken existing deny rules without discussion
- Distinctive: line 153
Update SECURITY.md if the threat model or defense layers change
- Very specific: line 155
Do not modify blocked-domains.txt without reviewing the domain's purpose.
- Very specific: line 162
Read such files with untrusted::read_untrusted(path, limit).
- Very specific: line 166
Before handing such a path to a subprocess like sqlite3, check it with symlink_metadata(..).is_file().
- Very specific: line 168
Never put their contents into the sandbox, an env var or output without that check.
- Distinctive: line 173
Add a test that plants a FIFO and a symlink.
- Very specific: line 204
Never let a backend difference decide whether a setting works.
- Very specific: line 247
Do not add Co-authored-by trailers to commits
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md
Rust project. A kernel-enforced sandbox for AI coding agents, using macOS Seatbelt (SBPL) and Linux Landlock plus seccomp-BPF and optional bubblewrap, with an HTTP CONNECT proxy in front of outbound traffic. It wraps Copilot, OpenCode, Antigravity, Pi, Claude Code, and a plain shell.
Build and test
Always run mise run check at the end of a coding session.
| Task | Suite | In sandbox | Linux CI | macOS CI | Requirements |
mise run check | fmt + clippy + unit + lib + offline probe | ✅ | ✅ | ✅ | Python 3 + Node.js (safe everywhere) |
mise run test:unit | unit_tests | ✅ | ✅ | ✅ | None |
mise run test:lib | lib (modules) | ✅ | ✅ | ✅ | None |
mise run test:probe | offline probe checks | ✅ | ✅ | ✅ | Python 3 + Node.js; PTY case macOS-only |
mise run test:integration | integration | ✅ | ❌ | ✅ | macOS sandbox-exec |
mise run test:integration-linux | integration_linux | ❌ | ✅ | ❌ | Linux with Landlock (5.13+) |
mise run test:network-audit | network_audit | ✅ | ❌ | ✅ | macOS or Linux; binds localhost TCP |
mise run test:e2e | e2e | ✅ | ❌ | ✅ | macOS + copilot in PATH; checkout not under /tmp or /var/folders |
mise run test:e2e-projects | e2e_projects | ✅ | ❌ | ✅ | macOS sandbox-exec; checkout not under /tmp or /var/folders |
mise run test:worktree-broker | worktree_broker | ❌ | ❌ | ✅ | macOS; real git, binds a Unix socket |
mise run test:e2e-live | e2e (ignored), 6 smoke tests | ❌ | ❌ | ⚠️ | macOS + Copilot auth + network |
mise run test:all | the suites above except live, including guard and broker suites | ✅ | ❌ | ✅ | macOS + copilot in PATH |
mise run test:everything | all + live | ❌ | ❌ | ⚠️ | macOS + Copilot auth + network |
mise run test | every suite via cargo test | ❌ | ❌ | ✅ | macOS |
Both e2e suites spawn a fake agent binary from inside the project tree, and the sandbox denies exec under /tmp and /var/folders by design. A checkout there fails nearly every test in e2e_projects and hangs e2e, with no hint that the location is the cause. Clone somewhere else.
test:all includes the gh/git guard, Git-hardening and worktree-broker suites.
Use their targeted tasks while iterating on those boundaries.
Non-test tasks:
mise run clippy # linter
mise run fmt # auto-format
mise run bench:startup # release startup latency + parent git spawn budget
mise run check:all-targets # cargo check every release target, to catch breakage on
# non-native targets (requires user setup)On a Mac where Little Snitch only allows the Homebrew cplt, run hack/dev-proxy.sh and pass
--with-proxy --proxy-upstream http://127.0.0.1:18443 to the dev build to give it network access.
Code style
- Rust 2024 edition, stable toolchain
- Standard rustfmt formatting (no overrides)
- Clippy must pass clean (warnings are errors in CI)
std::process::Command::newis adisallowed_methodslint (clippy.toml). Parent-side code resolves throughgit::trusted_git()/git::trusted_binary()or names an absolute path; where the program is already a resolved&Path, or where PATH resolution is deliberate (the guard wrappers run inside the sandbox), add#[allow(clippy::disallowed_methods)]with a one-line reason- Prefer
&str/&[T]over owned types in function signatures - Security-critical code gets doc comments explaining the why
- Only comment code that needs clarification
Project layout
The significant modules, not an exhaustive listing. src/lib.rs has the full
set.
src/sandbox.rsmodule root with re-exports (submodules use#[path])src/sandbox_policy.rsconstants, types, deny lists, env allowlists, validationsrc/sandbox_profile.rsSBPL profile generation (generate_profile, takesSandboxConfig)src/sandbox_env.rsenvironment variable construction (build_sandbox_env)src/sandbox_exec.rssandbox execution, validation, signal forwardingsrc/sandbox_landlock.rsLandlock LSM + seccomp-BPF (cross-platform policy, Linux-only enforcement)src/sandbox_bubblewrap.rsoptional bubblewrap namespace layer and in-namespace re-entry (Linux)src/config/config file parsing, CLI/config merging, key registry, per-repo keys,Resolvedstructsrc/worktree_broker.rsparent-sidecplt worktree addbroker and client (#531)src/discover.rsruntime environment probing (cplt doctor)src/detect.rsproject and machine ecosystem detectors behindcplt initsrc/gh_proxy.rsgh and git command guards, policy tables, wrapper scriptssrc/git.rshardened parent-side git invocation, the single choke pointsrc/trust.rs+src/repo_config.rs.cplt.tomlparsing and the trust storesrc/audit.rspost-session project-change reportsrc/check.rsstartup sandbox self-checksrc/init.rscplt initoutput generationsrc/gradle_init.rscplt-managed Gradle init scriptsrc/settings.rs+src/ui.rsinteractive settings TUI and terminal outputsrc/subscriptions.rsblocklist subscriptionssrc/update.rsself-update check and downloadsrc/scratch.rsper-session scratch directorysrc/agent.rsagent abstraction (Copilot, OpenCode, Antigravity, Pi, Claude Code, Shell). Binary discovery, config dirs, session flag translation, default domain allowlists, auth hints. Also holds the agent unit tests, which run as lib testssrc/main.rsCLI entry point, orchestrationsrc/lib.rslibrary crate root (re-exports modules for test access)src/proxy.rsCONNECT proxy, domain blocking, audit logtests/common/mod.rstest isolation helpers, shared by every suitetests/unit_tests.rscross-platform unit teststests/integration.rsmacOS sandbox-exec kernel-level teststests/integration_linux.rsLinux Landlock+seccomp kernel-level teststests/e2e.rsend-to-end with compiled binary, plus the ignored live smoke teststests/e2e_projects.rse2e tests with realistic project scaffoldingtests/e2e_guards.rsgh and git guard e2e teststests/e2e_git_hardening.rsparent-side git invocation hardeningSECURITY.mdthreat model, defense layers, honest gaps
Test strategy
The four tiers each prove something the others cannot, so all four are needed.
- Unit tests verify profile and policy generation: the SBPL profile text and Landlock policy contain the correct allow/deny rules, env vars are filtered properly, config merging works. Fast and cross-platform, but they do not prove the kernel actually enforces anything.
- Integration tests verify kernel enforcement by running a real command
inside the sandbox (macOS
sandbox-exec, Linux Landlock+seccomp) and asserting it is denied or allowed. This is the ground truth for security properties. - E2E tests verify the full binary pipeline: CLI arg parsing → profile generation → sandbox invocation → exit code. Catches wiring bugs between modules.
- E2E project tests verify that realistic developer workflows (git, node, python, rust file ops, config files) work inside the sandbox. Catches real-world breakage that synthetic tests miss.
Isolation
A test must not inherit the machine it runs on. Build every subprocess with the
helpers in tests/common/mod.rs, never Command::new directly:
cplt_cmd()— the binary withCPLT_CONFIGpointed at a dead end, so config resolution (CLI flag > config > preset > default) sees the defaults rather than the developer's~/.config/cplt/config.toml.cplt_cmd_with_ambient_config()— the explicit opt-out, for a test that is itself about config discovery.git_cmd(dir)— git withGIT_CONFIG_GLOBAL/GIT_CONFIG_NOSYSTEMcleared, socommit.gpgsign,core.hooksPathandinit.defaultBranchcannot decide whether fixture setup succeeds.temp_repo(remote)— a throwaway repo with a knownorigin. Anything that resolves a repository from the cwd belongs here, not in the checkout, whoseoriginisnavikt/cpltonly for people who cloned it directly.
Two failures came from skipping this (issue #245), and both reported success while exercising nothing.
Which tier to use:
- Adding or changing a sandbox rule → unit test for the policy string, plus an integration test for kernel enforcement
- Adding a config option → unit test for merge logic in
config/ - Adding env var filtering → unit test in
unit_tests.rs - Fixing a real-world breakage → e2e_projects test reproducing the scenario
- Adding a Landlock rule → unit test in
sandbox_landlock.rs, plus a test inintegration_linux.rs
Security constraints
This is a security tool. Changes to sandbox rules, env handling, or network policy must:
- Have a clear security rationale documented in the commit or a code comment
- Not weaken existing deny rules without discussion
- Update SECURITY.md if the threat model or defense layers change
Do not modify blocked-domains.txt without reviewing the domain's purpose.
Files the agent can write
cplt runs outside the sandbox, so treat every path the agent can write as hostile: the project, the scratch dir, agent config, state and data dirs, and cplt's own cache.
- Read such files with
untrusted::read_untrusted(path, limit). It doesn't follow symlinks, doesn't block on a FIFO, accepts plain files only and caps the size.open_untrusteddoes the same checks but returns aFilewith no cap: bound any read from it yourself. - Before handing such a path to a subprocess like
sqlite3, check it withsymlink_metadata(..).is_file().Path::is_file()follows symlinks. - Never put their contents into the sandbox, an env var or output without that check.
A symlinked
auth.jsononce leaked another tool's credentials this way. - Emit automatic file grants before the deny rules, as
file-read*. In Seatbelt a narrower allow such asfile-read-databeats a broader deny, whatever the order. Explicit overrides the user approved still go after the denies, on purpose. - Add a test that plants a FIFO and a symlink.
No silent grants
A grant or setting that cannot be honoured must fail loudly at the point it is written. It must never be accepted and then quietly dropped.
Accepting a line the user wrote and ignoring it is worse than refusing it. The user believes they have the access, or the restriction, that they asked for. Nothing tells them otherwise, and the belief survives until something breaks in a way that never points back here.
This holds in both directions. A grant that silently does nothing leaves someone thinking a path is reachable when it is not. A restriction that silently does nothing leaves someone thinking a path is closed when it is open. The second is the one that gets people hurt.
It has been rediscovered repeatedly rather than reasoned about, which is why it
is written down: allow.read on a hard-denied file worked on Linux and was
silently overridden on macOS (#207); --allow-socket emitted rules connect()
never consults, granting and denying nothing on Linux (#240); allow.write
inside a credential directory was inert on macOS while working on Linux (#291);
read-only grants under /tmp vanished inside bubblewrap's private tmpfs (#299);
cplt config set accepted a path the next launch refuses (#306); --inherit-env
cancelled an explicit --pass-env (#307); the entire [audit] section was
settable, displayed, and consumed by nothing (#309).
So, when adding a config key, a flag, or a grant kind:
- Refuse at the earliest point that can refuse — config-set time beats resolve time beats launch time. Say what is wrong and what to do instead.
- Never let a backend difference decide whether a setting works. If it cannot be
enforced on one platform, that is a stated gap with a reason, not an absence.
LinuxCoverage::Gap(reason)in the protected-path tables is the pattern. - A key with no consumer does not ship. If it must exist ahead of its implementation, mark it unimplemented everywhere it surfaces.
- Cover it with a test that fails when the setting stops taking effect. Every case above was found by a person tripping over it, never by the suite.
Staged rollout
A change that can break a working setup ships behind a config key, default off, and is enabled gradually. The security fix lands on its own merits; flipping the default is a separate, staged decision that needs maintainer sign-off.
The key is not required when every existing user has a proven, tested path forward: an automatic fallback to the old behavior, or a documented one-step recovery. Ship such a change as the new default, with the fallback tested and the recovery documented.
For a key-gated change, with the key unset generated profiles must be byte-identical to the previous release, and the PR is required to demonstrate that, not just assert it.
#264 (pinned) is the inventory of default-off keys and holds the promotion criteria. A PR that adds a default-off key adds its row there; a PR that flips a default links there and moves the row to "Already flipped". A default flips only when every promotion criterion in #264 holds.
Key patterns
(deny default)plus specific allows, so the sandbox is deny-by-defaultENV_ALLOWLIST, the only env vars that pass throughHARDENING_ENV_VARS, declarative security env injection. Add new entries hereHOME_TOOL_DIRSis a single unified list for both macOS and Linux. Put bothLibrary/*(macOS) and.cache/.local/share/*(XDG/Linux) paths in this one list. Do NOT create platform-specific duplicates. Non-existent paths are skipped at runtime.HomeToolDir, per-directory exec/map/write permissions- Network outbound scoping:
--allow-localhost-anyuses"localhost:*"for all cases. Java's IPv4-mapped address issue is solved by injecting-Djava.net.preferIPv4Stack=trueviaJAVA_TOOL_OPTIONS(macOS only), so"*:*"is no longer needed. - Config precedence: CLI flag > config file > default (secure default)
Git conventions
- Do not add
Co-authored-bytrailers to commits - GPG signing may be unavailable in the sandbox; use
git -c commit.gpgSign=false commitwhen needed
Docs
- README.md usage, flags, troubleshooting
- SECURITY.md threat model, attack analysis, defense layers
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repositoryOne other file it gives its agents.
- .github/copilot-instructions.mdCopilot instructions
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

