This document describes what Raven protects against, what it does not protect against, and why each boundary is what it is. It is written to be reviewable — the goal is an honest, auditable threat model, not a marketing claim.
Raven is a local coding-agent harness. It runs an LLM agent loop that can read, write, and execute commands in a workspace. The security question is: what can the agent do, and what stops it from doing more?
Raven's primary threat is a compromised or misbehaving model — the LLM producing tool calls that read, write, or execute something it shouldn't. This could be:
- Path traversal — the model asks to read/write a file outside the workspace.
- Symlink escape — a symlink inside the workspace points outside it.
- Shell injection — a command string smuggles extra commands.
- Exfiltration — the model reads a sensitive file and sends it somewhere.
- Resource exhaustion — a runaway command (fork bomb, huge write, infinite loop).
- Destructive commands —
rm -rf /,mkfs, etc.
Raven is not a defense against a malicious user who already has shell
access to the machine. If the user can run raven, they can run anything.
Raven confines the agent, not the user.
Raven applies several independent layers. Each is best-effort on some platforms; none is a complete boundary on its own. Defense-in-depth is the point.
What it does: On Linux, file opens go through openat2 with
RESOLVE_BENEATH | NO_MAGICLINKS, rooted at the workspace directory. The
kernel refuses to resolve any path that escapes the workspace — atomically,
with no TOCTOU race. A symlink cannot be swapped in between the check and the
open.
Platforms: Linux only. On other platforms, falls back to lexical ..
rejection + canonicalization of the nearest existing ancestor (see
safe_resolve), which is still a real check but has a small TOCTOU window.
Limitation: openat2 confines path resolution, not the process. A
confined child could still open a file it already has an fd to, or use a
different syscall. This is why Landlock (below) is the stronger boundary for
subprocesses.
What it does: Applies a Landlock ruleset to every subprocess before exec.
The child is granted read/write under the workspace, any explicit extra
roots (git worktree siblings), and /dev; read-only for /usr, /bin,
/lib, /lib64, /etc, /proc, and $HOME. Everything else is denied by
the kernel. The process temp dir is not granted — children get TMPDIR
pinned under workspace/.raven/tmp. Granting /tmp was a real escape:
run_shell could write /tmp/raven_eval_escape_probe.txt from a normal
home-directory workspace.
Why ABI V3 + pinned caches: Landlock ABI V1 does not include REFER.
Without it, rename/link across directories fails with EXDEV even under
the same allowed tree — which is how rustc stages .rmeta into target/.
Raven therefore:
- Uses Landlock ABI V3 so
AccessFs::from_allincludesREFER. - Pins
CARGO_HOME,CARGO_TARGET_DIR,TMPDIR, and the npm cache underworkspace/.raven/so package caches do not hardlink from$HOMEintotarget/across separate hierarchies. - Grants
$HOMEread-only (not RW) so git can read~/.gitconfigand rustup can read toolchains, without letting builds write caches into HOME.
Why /dev is allowed: git and many tools need /dev/null open for
read+write.
Escape hatch: RAVEN_SANDBOX_LANDLOCK=0 skips Landlock (tests / recovery).
Platforms: Linux only (Landlock is a Linux LSM, kernel 5.13+; REFER needs 5.19+ / ABI V2+).
Limitation: Best-effort. If the kernel doesn't support Landlock, Raven logs
a warning and continues without it. The CompatLevel::BestEffort mode means
unsupported features are silently downgraded — a security reviewer should
verify the kernel supports Landlock.
What it does: Applies a seccomp-BPF filter to every subprocess that denies
socket() when the domain is AF_INET or AF_INET6 with KillProcess
(immediate kill, not EPERM). AF_UNIX sockets are allowed (needed by
esbuild, vitest, git ssh helpers, etc.), and socketpair() is not blocked
(it only supports AF_UNIX on Linux). All other network syscalls (connect,
sendto, etc.) are allowed because no internet-facing socket can be created.
Why: This closes the exfiltration hole. Even if the model reads a sensitive file, the subprocess cannot create an internet-facing socket to send it anywhere over the network.
Platforms: Linux only (seccomp is a Linux feature). Supported arches: x86_64, aarch64, riscv64.
Limitation: Best-effort. If the arch is unsupported, Raven logs and continues without the filter. Also, the Raven process itself (not the subprocess) still has network access — the model API call goes through the parent process, which is not seccomp-confined.
Escape hatch: Set RAVEN_SANDBOX_NETWORK_BLOCK=0 to skip the filter
entirely (e.g. if a legitimate tool needs network access).
What it does: Applies RLIMIT_CPU (30s), RLIMIT_FSIZE (64 MiB), and
RLIMIT_NOFILE (1024) to every subprocess.
Why: Caps oversized writes (RLIMIT_FSIZE), runaway CPU (RLIMIT_CPU),
and fd exhaustion (RLIMIT_NOFILE).
Platforms: Linux + macOS.
Limitation: Best-effort — a kernel that doesn't support a limit is ignored.
Note on omitted limits: Memory and process counts are deliberately not
capped via setrlimit:
RLIMIT_AS(virtual address space) is omitted because runtimes like V8/Node reserve large regions up front — a 1 GiB cap made Node abort at startup. It bounds virtual, not resident, memory, so it is the wrong tool for limiting memory use anyway.RLIMIT_NPROC(processes/threads per user) is omitted because it is a user-global ceiling, not a per-child one. Imposing it on a child cannot isolate that child — a fork bomb would instead kill the entire user session — and on a busy machine it silently breaks high-thread runtimes (Node, etc.) because the ambient thread count is already near the cap.
The remaining layers bound the practical damage: Landlock confines the
filesystem, RLIMIT_CPU/RLIMIT_FSIZE stop runaway execution and writes, and
Windows Job Objects bound committed memory separately (see §5).
What it does: On Windows, every subprocess Raven spawns is assigned to a fresh Job Object configured with:
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE— all processes in the job are killed when the job handle closes, so a runaway child (and its entire process tree) cannot outlive the parent Raven process.JOB_OBJECT_LIMIT_ACTIVE_PROCESS(max 256) — caps the process tree size.JOB_OBJECT_LIMIT_PROCESS_MEMORY/JOB_OBJECT_LIMIT_JOB_MEMORY(1 GiB each) — caps per-process and per-job committed memory. These count resident (committed) memory, not virtual address space, so unlike the UnixRLIMIT_ASthey do not break runtimes like V8/Node that reserve large virtual regions.
The child is opened with the minimal access rights (PROCESS_SET_QUOTA | PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE) — deliberately not
PROCESS_ALL_ACCESS.
Why: Job Objects are the Windows-native equivalent of the Unix Landlock/seccomp/rlimits model. A process in a job cannot escape it, so a misbehaving child's grandchildren are confined to the same limits, and a fork-bomb/runaway process is killed when Raven exits.
Platforms: Windows only.
Limitation: Best-effort. If creating or assigning the Job Object fails (kernel policy, handle exhaustion), Raven logs a warning and the child runs without job confinement — the same posture as the Unix layers.
Residual risk (Windows filesystem and network): Job Objects are not a filesystem sandbox. There is no Landlock, AppContainer, Mandatory Integrity Control, or Restricted Token applied to the child. A confined Windows subprocess can:
- read and write any file the user can (
%USERPROFILE%, other drives, UNC paths,%TEMP%— which is not pinned the way LinuxTMPDIRis), - create
AF_INET/AF_INET6sockets (no seccomp equivalent), - follow symlinks / junctions / reparse points anywhere the user can.
File tools (read_file / write_file / …) still apply lexical .. rejection
plus canonicalization of the nearest existing ancestor (safe_resolve). That
is a real check but has a small TOCTOU window and does not bind the
subprocess. openat2/RESOLVE_BENEATH is Linux-only.
What this means in practice: a misbehaving model on Windows can exfiltrate
secrets over the network and write outside the workspace via run_shell.
Mitigations that actually help: keep confirm_shell on (do not pass --yolo),
run Raven as a low-privilege user, or run it inside Windows Sandbox / a VM /
a container. Do not treat Job Objects as "the Windows Landlock".
What it does:
- Denylist (
dangerous_re): blocks obviously destructive patterns (rm -rf /,mkfs, fork bombs,curl | sh,ddto block devices, etc.). This is a best-effort guard, not a security boundary — a denylist is inherently incomplete. - Allowlist (
safe_command_re): matches known-safe development commands. Whenconfirm_shellis enabled (the default, non---yolopath), commands matching the allowlist run without a confirmation prompt. Anything outside requires explicit user approval. - Direct exec: when a command's first token is on the allowlist AND the
command contains no shell metacharacters (
;,&,|,>,<, backticks,$, parens, braces,!,^, CR/LF/NUL), it is run viaCommand::new(bin).args(...)directly — no shell, no injection surface. This flips the model from "denylist dangerous" toward "allowlist safe" for the common case. - Never-execute denylist even under
--yolo: pipe-to-shell, reverse-shell primitives (/dev/tcp,nc -e,mkfifo), encoded PowerShell,certutil -decode, andInvoke-Expression/iex (are blocked before spawn. This is still a denylist, not a boundary. - Invocation log: every
run_shellthat actually starts (allowlisted, confirmed, or--yolo) is recorded in the sessiondebug-events.jsonlas a local-onlyshellevent. Declined confirmations are not logged as executed commands.
Platforms: All.
Limitation: The shell fallback path (commands with metacharacters) still
goes through sh -c (or cmd /C on Windows), which is inherently
injection-prone. The denylist is not a security boundary. The real safety net
is confirm_shell (user approval) and the OS-level layers above.
What it does: After staging (and excluding .env / .raven/ / data/),
git_commit and harness checkpoints scan staged files for well-known
credential prefixes (AWS AKIA…, GitHub ghp_/github_pat_, OpenAI /
Anthropic / OpenRouter / Stripe keys, PEM private-key headers, JWTs, …). A
match refuses the commit with a path + rule-name report. The secret value
is never copied into the tool result.
Limitation: Best-effort pattern match. Obfuscated, novel, or low-entropy
secrets will slip through. Pathspec exclusions still apply (.env is never
staged). The gate is fail-closed if the staged file list is too large to scan.
What it does: Before dispatch, Raven rejects tool calls whose arguments
JSON exceeds 1 MiB, are not a JSON object, or omit/mis-type required fields
(empty run_shell.command, missing read_file.path, …). Path arguments are
capped at 4096 characters; shell commands at 32 KiB.
Limitation: This is schema hygiene, not an allowlist of values. A
well-formed run_shell still has to pass the denylist / confirm_shell /
OS layers.
- No Windows filesystem confinement. Windows gets Job Objects (resource limits + process-tree confinement + kill-on-close) and shell filtering, but there is no Landlock/seccomp/AppContainer equivalent — a subprocess on Windows can still read/write any file the user can, follow junctions, and make network calls. See §5 residual risk. This is the same posture as most tools in this space.
- No container/VM isolation. Raven does not require Docker or any container runtime. If you need stronger isolation, run Raven inside a container or VM yourself.
- No network sandbox on macOS. seccomp is Linux-only; macOS gets rlimits but not the network block.
- No protection against a malicious user. If the user can run
raven, they can run anything.
For a locked-down environment (e.g. a government agency), the recommended deployment is:
- Run on Linux (kernel 5.13+) so Landlock + seccomp are active.
- Verify Landlock is enforced — check the log for
Landlock not enforcedwarnings at startup. - Keep
confirm_shellenabled (the default). Do not use--yolounless you fully trust the model and the workspace. - For the strongest isolation, run Raven inside a container or VM. Raven does not require this, but it adds a boundary that Raven itself does not provide.
Run the test suite to verify the confinement layers are active:
cargo test --lib tools::tests::confined_child
cargo test --lib tools::tests::open_beneath
cargo test --lib tools::tests::is_direct_exec_command
cargo test --lib tools::tests::confined_child_oversized_write_capped_by_fsize
cargo test --lib tools::secrets
cargo test --lib tools::tests::git_commit_refuses_secret
cargo test --lib tools::validateThese tests verify that a confined child cannot read outside the Landlock
allowlist, cannot make network syscalls, cannot write more than RLIMIT_FSIZE,
and that open_beneath rejects traversal/symlink escapes.