grith.aidocs

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:

  1. fork() a child process. The child immediately calls prctl(PR_SET_PTRACER, ...) and waits.
  2. Parent calls ptrace(PTRACE_SEIZE) on the child to attach without stopping.
  3. Child installs the seccomp-bpf filter (the "interesting syscalls" filter) via seccomp(SECCOMP_FILTER_FLAG_TSYNC, ...).
  4. Child execve()s the target binary (claude in our example).
  5. 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.
  6. If filters auto-allow: grith resumes the syscall (ptrace(PTRACE_SYSCALL, ...)) and the call runs normally.
  7. If filters auto-deny: grith rewrites the syscall to a no-op (e.g. getpid) and sets the return register to EACCES before resuming.
  8. 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 operationsopen, openat, openat2, unlink, unlinkat, rename, renameat2, chmod, fchmod, fchmodat.
  • Process operationsexecve, execveat, clone, fork, vfork, prlimit64 (for memory caps), kill.
  • Network operationssocket, connect, bind, sendto, sendmsg, getaddrinfo (intercepted at the libc layer via a special hook in DNS-aware builds).
  • Memory protectionmprotect with 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

Last updated: 2026-05-14Edit this page on GitHub →
© 2026 grith. All rights reserved.