Syscall interception
How grith catches what an agent does at the OS level — ptrace + seccomp on Linux.
grith intercepts every syscall an agent makes. That sentence does most of the work in this product. Everything else — the filters, the digest, the scoring — is downstream of being able to see and stop syscalls before they run.
This page explains how that interception works on Linux x86_64 (the v0.1 supported target). macOS and Windows backends are different mechanisms with the same goal; see Supported platforms for status.
The mechanism: ptrace + seccomp
The grith supervisor uses two complementary kernel features:
- ptrace lets one process attach to another and inspect/modify its execution state, including pausing the tracee before a syscall is committed.
- seccomp-bpf lets the kernel filter syscalls in-kernel using a BPF program. With
the right filter mode (
SECCOMP_RET_TRACE), interesting syscalls trap to the tracer and uninteresting ones run through at native speed.
The combination is fast: most syscalls (close, mmap, futex, sched_yield) bypass grith entirely because the seccomp filter says "don't bother the tracer about these". Only the syscalls grith cares about (open, openat, execve, connect, sendto, write to certain fds) trap to the supervisor for evaluation.
What grith actually does on grith exec
grith exec --profile claude-code -- claude
Behind the scenes:
fork()a child process. The child immediately callsprctl(PR_SET_PTRACER, ...)and waits.- Parent calls
ptrace(PTRACE_SEIZE)on the child to attach without stopping. - Child installs the seccomp-bpf filter (the "interesting syscalls" filter) via
seccomp(SECCOMP_FILTER_FLAG_TSYNC, ...). - Child
execve()s the target binary (claude in our example). - From this point on, every interesting syscall in the supervised tree raises a
ptrace-stop. grith's supervisor reads the syscall args, builds a
ToolCallType, sends it through the filter pipeline. - If filters auto-allow: grith resumes the syscall (
ptrace(PTRACE_SYSCALL, ...)) and the call runs normally. - If filters auto-deny: grith rewrites the syscall to a no-op (e.g.
getpid) and sets the return register to EACCES before resuming. - If filters quarantine: the thread stays stopped (
PTRACE_INTERRUPT) until a reviewer decides.
This is a slightly simplified version of what's in crates/grith-supervisor/. The
real code also handles PTY forwarding, signal forwarding, fork/clone tracking, and
the noise-reduction batching for rapid reads.
Process tree tracking
grith exec doesn't only supervise the immediate child. Any process the child
spawns inherits the seccomp filter and gets ptrace-attached automatically (via
PTRACE_O_TRACECLONE | PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK). An agent that
shells out to bash -c 'curl ... | sh' does not escape supervision — the bash, the
curl, and the resulting sh are all in the tracked tree.
The session ID identifies the tree. grith supervisor list shows one row per active
tree, with the root command line.
What's "interesting"
The seccomp filter intercepts (approximately):
- File operations —
open,openat,openat2,unlink,unlinkat,rename,renameat2,chmod,fchmod,fchmodat. - Process operations —
execve,execveat,clone,fork,vfork,prlimit64(for memory caps),kill. - Network operations —
socket,connect,bind,sendto,sendmsg,getaddrinfo(intercepted at the libc layer via a special hook in DNS-aware builds). - Memory protection —
mprotectwith W^X violations (advisory; off by default).
Reads — read, pread64 — are not in the interesting set by default, because the
data flow has already been captured at the open time. Tracking every read would
flood the pipeline. The taint filter inherits the read context from the file
descriptor's open.
PTY forwarding
When grith supervises an interactive program (REPL, shell, agent with a TUI), direct stdio inheritance wouldn't work — the supervisor sits between user and child. The supervisor allocates a master/slave PTY pair, hands the slave to the child, and shuttles bytes between the user's controlling TTY and the master.
The user sees no difference: their input goes to the agent, the agent's output comes back, ANSI escapes work, signals (Ctrl-C, Ctrl-Z) are forwarded.
The cost is one extra read+write per byte through the PTY. Negligible for
text-rate interaction; can be measured if you're pushing tens of MB through stdio,
which agents rarely do.
Noise reduction
A lot of syscalls are uninteresting in aggregate. Reading a 50-file directory of
configs touches openat 50 times. Each is a separate filter pass at base rates.
supervisor.noise_reduction knobs (ignore_read_only, batch_rapid_reads,
batch_window_ms) collapse rapid sequences of reads on the same FD into a single
filter pass per batch window. Default batch_window_ms = 50 is invisible to humans
and dramatically reduces filter work for read-heavy workloads.
Permissions
ptrace requires either CAP_SYS_PTRACE or matching uid + kernel.yama.ptrace_scope
allowing it. Most distros ship ptrace_scope = 1 ("only attach to descendants"),
which is what grith needs — and what it does, because the supervised process is
always a descendant of the supervisor.
Some hardened distros (some EC2 AMIs, Ubuntu hardening guides) ship
ptrace_scope = 2 ("admin-only"). That breaks grith exec. The fix:
sudo sysctl kernel.yama.ptrace_scope=1
Add to /etc/sysctl.d/ to persist.
What this is not
grith is not a sandbox. Allowed calls run on the real filesystem against the real network. The supervisor decides yes/no; the kernel still does the work.
This is intentional. Sandboxes have their own large attack surface (chroot escape, namespace escape, redirected mounts that confuse the agent). grith leaves the agent on the real system and trusts the kernel to keep it inside the OS process model. The security guarantee comes from policy, not from re-implementing the OS.
If you do want a sandbox layer, run grith inside a container or behind a Linux user namespace. The supervision and the sandbox are complementary.
What's coming on other platforms
- macOS — Endpoint Security framework, kext-free, requires entitlement. Status: in-flight; ships in v2.0.
- Windows — ETW + minifilter for fs/registry, plus a process injection hook for network. Status: design done; ships in v2.0.
- aarch64 Linux — needs the register backend (ptrace works; the BPF programs are arch-specific). Status: targeting v0.2.
See also
- Supervisor profiles — what the supervisor uses to decide quickly vs. fully scoring
- grith exec — CLI reference
- Supervisor-only security assessment — formal threat model