Kernel-enforced sandboxing for AI coding agents.
Sandy gives an agent access to your project without giving it unrestricted access to your computer. The sandbox applies to the agent and every command it starts. If Sandy cannot enforce the requested policy, the agent does not run.
Sandy is a foreground process sandbox, not a container, VM, daemon, or modified copy of the agent.
Install Sandy on macOS with Homebrew:
brew install kontext-security/tap/sandyLinux x86-64 and arm64 archives and their SHA-256 checksums are available from
GitHub Releases. Unpack
the archive and place sandy on your PATH.
Verify that the native sandbox is available:
sandy doctorThen run an agent from your project:
# macOS
sandy run -- claude
sandy run -- codex --sandbox danger-full-access
sandy run -- opencode
# Linux v0.2 (after the one-time setup below)
sandy run -- codex --sandbox danger-full-accessOn Linux, create Codex's protected control files before the first Sandy launch.
Sandy does not create or overwrite agent configuration during run:
mkdir -p ~/.codex
test -e ~/.codex/config.toml || touch ~/.codex/config.toml
test -e ~/.codex/hooks.json || printf '{}\n' > ~/.codex/hooks.jsonThe current project is writable. Network access is allowed unless you block it explicitly:
sandy run --block-net -- claudeGrant additional filesystem access with Sandy options before --. Everything
after -- is passed to the target unchanged.
sandy run --read ../shared --read-write ~/Downloads/output -- claude
sandy run --dry-run -- claudeThe Rust library restricts the calling process directly. It does not require the Sandy executable, a daemon, or a bootstrap hook.
[dependencies]
sandy = { package = "sandy-sandbox", version = "0.2" }use sandy::{AccessMode, NetworkPolicy, PathScope, SandboxPolicy};
let workspace = std::env::current_dir()?;
let policy = SandboxPolicy::new(NetworkPolicy::BlockAll).grant(
&workspace,
AccessMode::ReadWrite,
PathScope::Subtree,
);
sandy::apply(policy)?;
run_untrusted_work();
# fn run_untrusted_work() {}
# Ok::<(), Box<dyn std::error::Error>>(())apply is irreversible. Call it before starting threads, opening sensitive
resources, or running untrusted work. The caller owns the complete policy;
Sandy adds no implicit application baseline.
Policies can live outside application code as strict, versioned JSON. This
policy makes the current workspace writable, blocks network access, permits
subprocesses and tools below ./tools, and keeps settings.json read-only:
{
"schema_version": 1,
"network": "block_all",
"allow_subprocesses": true,
"grants": [
{
"path": ".",
"access": "read_write",
"scope": "subtree"
}
],
"executable_grants": [
{
"path": "./tools",
"scope": "subtree"
}
],
"deny_write_exact": [
"./settings.json"
]
}Embed the document in a Rust binary and apply it without reading policy from the host at runtime:
use sandy::SandboxPolicy;
let policy = SandboxPolicy::from_json(include_bytes!("sandbox.json"))?;
sandy::apply(policy)?;
run_untrusted_work();
# fn run_untrusted_work() {}
# Ok::<(), Box<dyn std::error::Error>>(())Policy paths are resolved when apply runs and must already exist. File access
does not imply executable access. Unknown fields, unsupported versions, and
unrepresentable policy combinations are rejected rather than approximated.
This complete library policy is not a CLI --profile-file: the CLI format is
an additive extension of one built-in profile and cannot set network or process
policy. See the public Rust API and CLI profile
format for the two contracts.
Kernel enforcement is foundational, but it cannot determine whether an otherwise permitted action is suspicious, whether a sequence of actions forms an attack, or which policy should govern a specific agent decision. Sandy therefore keeps behavioral detection and authorization separate and composable:
- Numbat provides endpoint visibility, on-device detection, optional pre-action blocking, and forensic reconstruction across hooks, session artifacts, and telemetry from OpenTelemetry exporters over OTLP/HTTP.
- Kontext adds identity-aware, pre-action authorization with Cedar policies at supported agent hooks and records the decision and available outcome in an authorization ledger.
These controls reason about behavior and intent. Sandy remains the native boundary that limits what the process can physically access. On macOS, Sandy can preserve verified existing hooks for these tools; neither is required to use the sandbox.
✅ Supported · ❌ Not supported
| Agent | macOS | Linux v0.2 |
|---|---|---|
| Claude Code | ✅ | ❌ |
| Codex | ✅ | ✅ |
| OpenCode | ✅ | ❌ |
| Interface | macOS | Linux v0.2 |
|---|---|---|
| CLI sandbox runner | ✅ | ✅ |
Rust library (sandy::apply) |
✅ | ✅ |
| Versioned JSON library policies | ✅ | ✅ |
| CLI profile files | ✅ | ✅ |
| Filesystem read, write, and execute policy | ✅ | ✅ |
| Network allow/block | ✅ | ✅ |
| Integration | macOS | Linux v0.2 |
|---|---|---|
| Kontext | ✅ | ❌ |
| Numbat | ✅ | ❌ |
Linux CI executes a real pinned Codex binary on x86-64 and arm64. Codex completes a provider-backed agent turn, invokes its command tool, writes inside the granted project, and is denied when it tries to write outside it. Claude Code and OpenCode currently require dynamic process paths that Sandy's private Linux filesystem does not expose.
Read CONTRIBUTING.md before opening a pull request. Changes to enforcement must update behavior, tests, and documentation together.
make checkSandy is experimental security software and has not completed an independent audit. It is a process sandbox, not a separate kernel, user account, or memory boundary. The macOS backend uses Apple's private, deprecated Seatbelt interface. Linux has an explicit host and compatibility contract documented in the Linux security model.
Read the threat model before relying on Sandy for a security boundary. Report vulnerabilities privately as described in SECURITY.md; do not open a public issue for an unpatched vulnerability.
