agentleFS
naviktnavikt/cplt/AGENTS.md

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
  1. Very specific:

    Always run mise run check at the end of a coding session.

    line 10
  2. Distinctive:

    Use their targeted tasks while iterating on those boundaries.

    line 32
  3. Very specific:

    Prefer &str/&[T] over owned types in function signatures

    line 57
  4. Distinctive:

    Only comment code that needs clarification

    line 59
  5. Distinctive:

    Unit tests: verify profile and policy generation: the SBPL profile text and Landlock policy contain the correct allow/deny rules, env vars …

    line 106
  6. Very specific:

    Integration tests: verify kernel enforcement by running a real command inside the sandbox (macOS sandbox-exec, Linux Landlock+seccomp) and …

    line 110
  7. Distinctive:

    E2E tests: verify the full binary pipeline: CLI arg parsing → profile generation → sandbox invocation → exit code.

    line 113
  8. Distinctive:

    E2E project tests: verify that realistic developer workflows (git, node, python, rust file ops, config files) work inside the sandbox.

    line 115
  9. Distinctive:

    Not weaken existing deny rules without discussion

    line 152
  10. Distinctive:

    Update SECURITY.md if the threat model or defense layers change

    line 153
  11. Very specific:

    Do not modify blocked-domains.txt without reviewing the domain's purpose.

    line 155
  12. Very specific:

    Read such files with untrusted::read_untrusted(path, limit).

    line 162
  13. Very specific:

    Before handing such a path to a subprocess like sqlite3, check it with symlink_metadata(..).is_file().

    line 166
  14. Very specific:

    Never put their contents into the sandbox, an env var or output without that check.

    line 168
  15. Distinctive:

    Add a test that plants a FIFO and a symlink.

    line 173
  16. Very specific:

    Never let a backend difference decide whether a setting works.

    line 204
  17. Very specific:

    Do not add Co-authored-by trailers to commits

    line 247

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

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.

TaskSuiteIn sandboxLinux CImacOS CIRequirements
mise run checkfmt + clippy + unit + lib + offline probe✅✅✅Python 3 + Node.js (safe everywhere)
mise run test:unitunit_tests✅✅✅None
mise run test:liblib (modules)✅✅✅None
mise run test:probeoffline probe checks✅✅✅Python 3 + Node.js; PTY case macOS-only
mise run test:integrationintegration✅❌✅macOS sandbox-exec
mise run test:integration-linuxintegration_linux❌✅❌Linux with Landlock (5.13+)
mise run test:network-auditnetwork_audit✅❌✅macOS or Linux; binds localhost TCP
mise run test:e2ee2e✅❌✅macOS + copilot in PATH; checkout not under /tmp or /var/folders
mise run test:e2e-projectse2e_projects✅❌✅macOS sandbox-exec; checkout not under /tmp or /var/folders
mise run test:worktree-brokerworktree_broker❌❌✅macOS; real git, binds a Unix socket
mise run test:e2e-livee2e (ignored), 6 smoke tests❌❌⚠️macOS + Copilot auth + network
mise run test:allthe suites above except live, including guard and broker suites✅❌✅macOS + copilot in PATH
mise run test:everythingall + live❌❌⚠️macOS + Copilot auth + network
mise run testevery 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::new is a disallowed_methods lint (clippy.toml). Parent-side code resolves through git::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.rs module root with re-exports (submodules use #[path])
  • src/sandbox_policy.rs constants, types, deny lists, env allowlists, validation
  • src/sandbox_profile.rs SBPL profile generation (generate_profile, takes SandboxConfig)
  • src/sandbox_env.rs environment variable construction (build_sandbox_env)
  • src/sandbox_exec.rs sandbox execution, validation, signal forwarding
  • src/sandbox_landlock.rs Landlock LSM + seccomp-BPF (cross-platform policy, Linux-only enforcement)
  • src/sandbox_bubblewrap.rs optional bubblewrap namespace layer and in-namespace re-entry (Linux)
  • src/config/ config file parsing, CLI/config merging, key registry, per-repo keys, Resolved struct
  • src/worktree_broker.rs parent-side cplt worktree add broker and client (#531)
  • src/discover.rs runtime environment probing (cplt doctor)
  • src/detect.rs project and machine ecosystem detectors behind cplt init
  • src/gh_proxy.rs gh and git command guards, policy tables, wrapper scripts
  • src/git.rs hardened parent-side git invocation, the single choke point
  • src/trust.rs + src/repo_config.rs .cplt.toml parsing and the trust store
  • src/audit.rs post-session project-change report
  • src/check.rs startup sandbox self-check
  • src/init.rs cplt init output generation
  • src/gradle_init.rs cplt-managed Gradle init script
  • src/settings.rs + src/ui.rs interactive settings TUI and terminal output
  • src/subscriptions.rs blocklist subscriptions
  • src/update.rs self-update check and download
  • src/scratch.rs per-session scratch directory
  • src/agent.rs agent 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 tests
  • src/main.rs CLI entry point, orchestration
  • src/lib.rs library crate root (re-exports modules for test access)
  • src/proxy.rs CONNECT proxy, domain blocking, audit log
  • tests/common/mod.rs test isolation helpers, shared by every suite
  • tests/unit_tests.rs cross-platform unit tests
  • tests/integration.rs macOS sandbox-exec kernel-level tests
  • tests/integration_linux.rs Linux Landlock+seccomp kernel-level tests
  • tests/e2e.rs end-to-end with compiled binary, plus the ignored live smoke tests
  • tests/e2e_projects.rs e2e tests with realistic project scaffolding
  • tests/e2e_guards.rs gh and git guard e2e tests
  • tests/e2e_git_hardening.rs parent-side git invocation hardening
  • SECURITY.md threat model, defense layers, honest gaps

Test strategy

The four tiers each prove something the others cannot, so all four are needed.

  1. 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.
  2. 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.
  3. E2E tests verify the full binary pipeline: CLI arg parsing → profile generation → sandbox invocation → exit code. Catches wiring bugs between modules.
  4. 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 with CPLT_CONFIG pointed 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 with GIT_CONFIG_GLOBAL/GIT_CONFIG_NOSYSTEM cleared, so commit.gpgsign, core.hooksPath and init.defaultBranch cannot decide whether fixture setup succeeds.
  • temp_repo(remote) — a throwaway repo with a known origin. Anything that resolves a repository from the cwd belongs here, not in the checkout, whose origin is navikt/cplt only 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 in integration_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_untrusted does the same checks but returns a File with no cap: bound any read from it yourself.
  • Before handing such a path to a subprocess like sqlite3, check it with symlink_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.json once leaked another tool's credentials this way.
  • Emit automatic file grants before the deny rules, as file-read*. In Seatbelt a narrower allow such as file-read-data beats 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-default
  • ENV_ALLOWLIST, the only env vars that pass through
  • HARDENING_ENV_VARS, declarative security env injection. Add new entries here
  • HOME_TOOL_DIRS is a single unified list for both macOS and Linux. Put both Library/* (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-any uses "localhost:*" for all cases. Java's IPv4-mapped address issue is solved by injecting -Djava.net.preferIPv4Stack=true via JAVA_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-by trailers to commits
  • GPG signing may be unavailable in the sandbox; use git -c commit.gpgSign=false commit when needed

Docs

  • README.md usage, flags, troubleshooting
  • SECURITY.md threat model, attack analysis, defense layers

Download now (ZIP)

  • 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.

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.