<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dan</title>
    <description>The latest articles on DEV Community by Dan (@djf73).</description>
    <link>https://dev.to/djf73</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4098796%2Ff0abd3d3-b0c8-45f5-bac6-490bb89325a3.png</url>
      <title>DEV Community: Dan</title>
      <link>https://dev.to/djf73</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/djf73"/>
    <language>en</language>
    <item>
      <title>73% of What We Blocked in a Month Was One Syscall Your Sandbox Cannot See</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Wed, 02 Sep 2026 12:08:41 +0000</pubDate>
      <link>https://dev.to/djf73/73-of-what-we-blocked-in-a-month-was-one-syscall-your-sandbox-cannot-see-4fb</link>
      <guid>https://dev.to/djf73/73-of-what-we-blocked-in-a-month-was-one-syscall-your-sandbox-cannot-see-4fb</guid>
      <description>&lt;p&gt;For the last month, every AI coding session on this machine ran under &lt;a href="https://grith.ai" rel="noopener noreferrer"&gt;grith&lt;/a&gt;, which intercepts every syscall an agent makes and scores it before the kernel acts. That is 2,129,319 intercepted events across 29 days, most of them from Claude Code and Codex doing ordinary work.&lt;/p&gt;

&lt;p&gt;18,518 of those events were denied. We expected the denials to be the interesting part - a credential read here, a connection to somewhere it should not go there. They were not. &lt;strong&gt;73% of everything we blocked was a single syscall: io_uring_setup.&lt;/strong&gt; 13,613 attempts, every one refused.&lt;/p&gt;

&lt;p&gt;That is worth sitting with, because io_uring is not an obscure edge case. It is the one I/O interface that a seccomp-based sandbox - the standard way people contain untrusted processes on Linux - cannot see into at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why seccomp cannot see io_uring
&lt;/h2&gt;

&lt;p&gt;A normal syscall sandbox works because the application has to ask the kernel for everything through syscalls, and seccomp inspects each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent -&amp;gt; openat("/home/you/.ssh/id_ed25519")  -&amp;gt; seccomp inspects -&amp;gt; allow / deny
agent -&amp;gt; connect(1.2.3.4:443)                  -&amp;gt; seccomp inspects -&amp;gt; allow / deny
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The security model rests on a single assumption: the application cannot perform an operation it never makes a syscall for.&lt;/p&gt;

&lt;p&gt;io_uring breaks that assumption. You call &lt;code&gt;io_uring_setup&lt;/code&gt; once to create a ring - a pair of queues in memory shared between the process and the kernel. After that, the process submits operations by writing entries into the ring: &lt;code&gt;IORING_OP_OPENAT&lt;/code&gt;, &lt;code&gt;IORING_OP_CONNECT&lt;/code&gt;, &lt;code&gt;IORING_OP_READ&lt;/code&gt;, &lt;code&gt;IORING_OP_WRITE&lt;/code&gt;, and dozens more. The kernel executes them asynchronously. With submission-queue polling enabled, the kernel picks the entries up on its own and &lt;strong&gt;no further syscall is made at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So a seccomp filter that carefully denies &lt;code&gt;openat&lt;/code&gt; and &lt;code&gt;connect&lt;/code&gt; is trivially bypassed: do not call &lt;code&gt;openat&lt;/code&gt;, submit &lt;code&gt;IORING_OP_OPENAT&lt;/code&gt; to the ring instead. The read of your SSH key happens, and the filter watching &lt;code&gt;openat&lt;/code&gt; sees nothing, because &lt;code&gt;openat&lt;/code&gt; was never called.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent -&amp;gt; io_uring_setup()          -&amp;gt; ring created
agent -&amp;gt; [ IORING_OP_OPENAT ] ---.
agent -&amp;gt; [ IORING_OP_CONNECT ] --+-&amp;gt; kernel executes, no syscall per op
agent -&amp;gt; [ IORING_OP_READ ] -----'    seccomp sees nothing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is exactly one place a seccomp filter can intervene: &lt;code&gt;io_uring_setup&lt;/code&gt; itself, which is a real syscall. Block the ring from ever being created, and none of the rest can happen. Allow it, and you have handed the process an I/O channel your policy does not govern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The industry already decided this
&lt;/h2&gt;

&lt;p&gt;If this sounds like an argument someone would resist, they mostly have not. Blocking io_uring at the sandbox boundary is now the default position across the ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; blocks the io_uring syscalls in its default seccomp profile as of 25.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;containerd&lt;/strong&gt; did the same for its &lt;code&gt;RuntimeDefault&lt;/code&gt; profile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; turned io_uring off in ChromeOS, Android, and across production servers, citing the volume of exploitable vulnerabilities and the difficulty of sandboxing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The consensus is not that io_uring is bad engineering - it is a genuinely excellent piece of high-performance I/O. The consensus is that its security model and the seccomp security model do not compose, so anything relying on seccomp to contain an untrusted process has to refuse io_uring outright. That is a trade-off - you lose the performance - and for a sandbox it is the correct one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both agents reach for it constantly
&lt;/h2&gt;

&lt;p&gt;Here is where the month of data matters. io_uring is not a theoretical concern you might one day encounter. In real AI coding sessions it comes up in nearly every one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code: 137 of 150 sessions&lt;/strong&gt; attempted io_uring, 11,162 attempts in total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex: all 31 of 31 sessions&lt;/strong&gt;, 2,451 attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The median session made 8 or 9 attempts. The busiest - a long Claude Code session - made &lt;strong&gt;1,713&lt;/strong&gt;, spread across two and a half hours of work. Whatever is reaching for io_uring is doing so as a matter of course, over and over, in ordinary sessions doing ordinary tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not an escape attempt, and that is the point
&lt;/h2&gt;

&lt;p&gt;We are not going to tell you Claude Code and Codex are trying to break out of a sandbox. They almost certainly are not. io_uring attempts at this volume have the fingerprint of an async runtime reaching for the fastest available I/O backend - the sort of thing libuv, or a Rust async runtime, or a language standard library does on your behalf without the agent, or its authors, thinking about it at all. It is a performance optimisation, not an exploit.&lt;/p&gt;

&lt;p&gt;That is exactly why it matters. &lt;strong&gt;The soundness of a sandbox cannot depend on the intent of the thing inside it.&lt;/strong&gt; A benign runtime reaching for io_uring to go faster and a deliberate attempt to route a credential read around your filter submit the identical operation to the identical ring. Your seccomp policy cannot tell them apart, because from the syscall layer it sees neither. If the ring exists, your enforcement has a hole in it, and whether that hole is used for good or ill is not a question your security model gets to ask.&lt;/p&gt;

&lt;p&gt;Which means the only safe assumption is the pessimistic one: if an agent's runtime can open a ring, treat your syscall-level policy as advisory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What grith does about it
&lt;/h2&gt;

&lt;p&gt;grith enforces at the syscall level too - ptrace with a seccomp-BPF pre-filter - so it inherits exactly the same blind spot. An io_uring ring would let the agent's I/O flow past grith's own scoring the same way it would past any other seccomp sandbox. grith's answer is the industry's answer: it refuses to let the ring open.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;io_uring_setup&lt;/code&gt; is denied structurally, before it is ever scored. In the audit log every one of the 13,613 attempts carries the same shape - syscall 425, hard-denied, EPERM returned, &lt;code&gt;io_uring denied before proxy evaluation&lt;/code&gt;. There is no threshold to tune and no context that makes it allowable, because allowing it would quietly blind everything else grith does. It is one of the few operations grith treats as non-negotiable rather than scoring on a curve.&lt;/p&gt;

&lt;p&gt;The cost is real and worth stating: an agent runtime that genuinely wanted io_uring for performance does not get it under grith, and falls back to ordinary syscalls. In exchange, the file reads and network connects that follow are ones grith can actually see. For a supervisor, that is the whole job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for you
&lt;/h2&gt;

&lt;p&gt;If you run AI coding agents inside any seccomp-based sandbox - a container with a custom profile, firejail, a homegrown wrapper - check whether it blocks io_uring. Recent Docker and containerd do by default; an older base image or a hand-rolled profile may not. If it does not, the careful &lt;code&gt;openat&lt;/code&gt; and &lt;code&gt;connect&lt;/code&gt; rules you wrote are enforceable only for as long as the agent's runtime chooses to use &lt;code&gt;openat&lt;/code&gt; and &lt;code&gt;connect&lt;/code&gt;, and this month's numbers say it reaches for the alternative thousands of times a session.&lt;/p&gt;

&lt;p&gt;The broader lesson is the one behind everything we measure at the syscall boundary: the interesting security questions about AI agents are not answered by what the agent says it is doing, but by what its process actually asks the kernel for - including the requests it makes through channels most tools are not watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docker default seccomp profile blocking io_uring: &lt;a href="https://github.com/moby/moby/pull/46762" rel="noopener noreferrer"&gt;moby/moby #46762&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;containerd RuntimeDefault profile: &lt;a href="https://github.com/containerd/containerd/pull/9320" rel="noopener noreferrer"&gt;containerd/containerd #9320&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;io_uring and seccomp, a technical walkthrough: &lt;a href="https://blog.0x74696d.com/posts/iouring-and-seccomp/" rel="noopener noreferrer"&gt;blog.0x74696d.com/posts/iouring-and-seccomp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About grith
&lt;/h2&gt;

&lt;p&gt;grith is an open-source security supervisor for AI coding agents. It sits underneath the agent rather than inside it: on Linux, ptrace with a seccomp-BPF pre-filter intercepts every syscall the agent makes and scores it against 18 filters before the kernel executes it. Operations it cannot safely allow - io_uring ring creation among them - are refused at the boundary, and everything it evaluates lands in a local audit log you can reconstruct a month of sessions from, which is exactly what this post is.&lt;/p&gt;

&lt;p&gt;It is Rust, MPL-2.0, a single static binary, and runs entirely on your own machine. Linux x86_64 and aarch64 today.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/grith-ai/grith" rel="noopener noreferrer"&gt;github.com/grith-ai/grith&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site and docs: &lt;a href="https://grith.ai" rel="noopener noreferrer"&gt;grith.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try it: &lt;code&gt;curl -fsSL https://grith.ai/install | sh&lt;/code&gt;, then &lt;code&gt;grith exec -- claude-code "fix the bug"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://grith.ai/blog/io-uring-the-syscall-your-sandbox-cant-see" rel="noopener noreferrer"&gt;grith.ai/blog/io-uring-the-syscall-your-sandbox-cant-see&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>linux</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Claude Code Attempted 752 /proc/*/environ Reads. 256 Succeeded. Codex: 0.</title>
      <dc:creator>Dan</dc:creator>
      <pubDate>Fri, 28 Aug 2026 11:07:31 +0000</pubDate>
      <link>https://dev.to/djf73/claude-code-attempted-752-procenviron-reads-256-succeeded-codex-0-4kob</link>
      <guid>https://dev.to/djf73/claude-code-attempted-752-procenviron-reads-256-succeeded-codex-0-4kob</guid>
      <description>&lt;p&gt;We asked Claude Code to add input validation to a single route handler. The task required editing one file. Roughly 20 lines of code.&lt;/p&gt;

&lt;p&gt;Here is what happened before it wrote a single character.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;We built a standardised benchmark: a small Node.js/Express user management service with a &lt;code&gt;POST /users&lt;/code&gt; endpoint that was deliberately missing input validation. The task was identical for both agents: add validation for &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, and &lt;code&gt;password&lt;/code&gt;, return 400 with a descriptive error for each failure case.&lt;/p&gt;

&lt;p&gt;We ran each agent under &lt;code&gt;strace -f&lt;/code&gt;, recording every &lt;code&gt;openat&lt;/code&gt;, &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;execve&lt;/code&gt;, and &lt;code&gt;getdents64&lt;/code&gt; call made by the agent process and every subprocess it spawned&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. All runs used a fresh copy of the project. We ran Claude Code twice to check stability; the results were within 0.5% across both runs.&lt;/p&gt;

&lt;p&gt;Agents tested: Claude Code 2.1.72 and Codex CLI 0.113.0&lt;sup id="fnref2"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;The benchmark project and analysis scripts are &lt;a href="https://github.com/grith-ai/grith-website/tree/main/scripts/trace" rel="noopener noreferrer"&gt;published on GitHub&lt;/a&gt; if you want to run this yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Task duration&lt;/td&gt;
&lt;td&gt;17.9s&lt;/td&gt;
&lt;td&gt;42.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Files read (unique)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,779&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;303&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files written&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files in the project&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files outside the project&lt;/td&gt;
&lt;td&gt;2,782&lt;/td&gt;
&lt;td&gt;319&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directory scans&lt;/td&gt;
&lt;td&gt;368&lt;/td&gt;
&lt;td&gt;750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unique subprocesses spawned&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Claude Code opened 2,779 unique files in the process of editing one. Codex opened 303.&lt;/p&gt;

&lt;p&gt;Before interpreting the gap: most of Claude Code's reads are its own infrastructure - plugin cache, config files, node module resolution. The absolute number is not the finding. What the agent read is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: Claude Code scanned the environment of 752 running processes
&lt;/h2&gt;

&lt;p&gt;During the session, Claude Code attempted to open &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/environ&lt;/code&gt; for &lt;strong&gt;752 distinct process IDs&lt;/strong&gt; - every process visible in &lt;code&gt;/proc&lt;/code&gt;, from PID 1 upward. Most kernel and root-owned processes returned &lt;code&gt;EACCES&lt;/code&gt;, but &lt;strong&gt;256 succeeded&lt;/strong&gt; - every process running under the current user. Across two runs, the numbers were 752/756 attempted and 256/254 successful. It did not vary meaningfully because it is not random. Claude Code walks &lt;code&gt;/proc&lt;/code&gt; systematically on startup to inherit the parent shell's environment variables.&lt;/p&gt;

&lt;p&gt;The mechanism is legitimate. Shells export environment variables - &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;NODE_ENV&lt;/code&gt;, API keys you have exported in your &lt;code&gt;.zshrc&lt;/code&gt; - and a terminal-launched process cannot read them any other way on Linux. So Claude Code walks &lt;code&gt;/proc&lt;/code&gt; to find them.&lt;/p&gt;

&lt;p&gt;But it does not read just the parent shell's environment. It reads the environment of every user-owned process it can access. On the test machine, the 256 successfully read processes included:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Process&lt;/th&gt;
&lt;th&gt;Instances&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Firefox&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;Browser (tabs, extensions, GPU process)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Editor and extension hosts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoom&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Video conferencing (webview hosts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Messaging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Other Claude sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Other Codex sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gnome-keyring-daemon&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;System credential store&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bash / tmux&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;Terminal sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Node.js processes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DBeaver (Java)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Database client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gnome-keyring-daemon entry is worth highlighting. This is the process that manages your system keychain - SSH keys, GPG keys, Wi-Fi passwords, and any secret stored via the GNOME keyring API. Claude Code successfully opened its &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/environ&lt;/code&gt;, which contains the &lt;code&gt;DBUS_SESSION_BUS_ADDRESS&lt;/code&gt; and other variables needed to interact with the keyring over D-Bus. Reading the environ does not extract stored secrets directly, but it provides the addressing information an attacker would need to query the keyring programmatically.&lt;/p&gt;

&lt;p&gt;It also read &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/stat&lt;/code&gt;, &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt;, and &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cmdline&lt;/code&gt; for processes starting from PID 1 - process names, states, and full command lines for the entire running process table.&lt;/p&gt;

&lt;p&gt;Codex did not do any of this. Zero &lt;code&gt;/proc/*/environ&lt;/code&gt; reads in either run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: Both agents read credentials the task never required
&lt;/h2&gt;

&lt;p&gt;Neither agent needed any of the following to add input validation to a route. Both read them anyway.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.gitconfig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;x49&lt;/td&gt;
&lt;td&gt;x47&lt;/td&gt;
&lt;td&gt;Each of the 16-18 &lt;code&gt;git&lt;/code&gt; subprocesses resolves the global config independently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/etc/passwd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;x20&lt;/td&gt;
&lt;td&gt;x16&lt;/td&gt;
&lt;td&gt;Every subprocess resolves the current user via &lt;code&gt;getpwuid()&lt;/code&gt; on startup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.npmrc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;x1&lt;/td&gt;
&lt;td&gt;x1&lt;/td&gt;
&lt;td&gt;npm registry auth token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.ssh/config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;x2&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;SSH config, read during git remote operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.ssh/known_hosts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;x4&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;SSH host verification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;~/.gitconfig&lt;/code&gt; and &lt;code&gt;/etc/passwd&lt;/code&gt; counts are not targeted reads - they are the noise floor of running Node.js and git in a subprocess tree. Each of the 14-18 subprocesses both agents spawned does its own user resolution. You would see similar numbers from a shell script that called git 16 times.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;~/.npmrc&lt;/code&gt; read is more interesting. The task involved no npm publishing. The file contains a registry authentication token. It was opened because npm was invoked once to check the project's dependency state, and npm reads its config unconditionally on startup. Neither agent needed the token. Both opened the file that contains it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: Claude Code initialised Gmail and Google Calendar for a coding task
&lt;/h2&gt;

&lt;p&gt;After the session completed, we found two log files written outside the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.cache/claude-cli-nodejs/.../mcp-logs-claude-ai-Gmail/2026-03-10T14-28-34-414Z.jsonl
~/.cache/claude-cli-nodejs/.../mcp-logs-claude-ai-Google-Calendar/2026-03-10T14-28-34-414Z.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code initialises every configured MCP server at session startup, before the task begins. The machine running this test had Gmail and Google Calendar MCP servers configured. Both were touched during a session whose only purpose was to add three if-statements to a route file.&lt;/p&gt;

&lt;p&gt;The network trace corroborates this: one of Claude Code's outbound connections was to a Google Cloud IP (&lt;code&gt;137.66.149.34.bc.googleusercontent.com:443&lt;/code&gt;), which does not appear in Codex's trace.&lt;/p&gt;

&lt;p&gt;MCP servers can have significant access - reading your email, querying your calendar, accessing authenticated services. Whether or not they were actually used in this session, they were initialised. The attack surface of your coding session includes whatever MCP servers you have configured, on every session, regardless of the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4: Claude Code walked up to the parent git repository
&lt;/h2&gt;

&lt;p&gt;The benchmark project was a subdirectory of a larger git repository. Claude Code found and read the parent project's &lt;code&gt;.git/HEAD&lt;/code&gt; and &lt;code&gt;.git/config&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is standard git behaviour - &lt;code&gt;git&lt;/code&gt; walks up the directory tree to find the nearest &lt;code&gt;.git&lt;/code&gt; directory. But it means that in a real working environment where the project lives inside a monorepo or a larger workspace, the agent is reading git metadata - remote URLs, credentials embedded in remote configs, custom hooks - from repositories above the project it was asked to work on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 5: Claude Code ran a background plugin sync during the task
&lt;/h2&gt;

&lt;p&gt;During the session, Claude Code acquired git locks and wrote &lt;code&gt;ORIG_HEAD.lock&lt;/code&gt; files to two plugin marketplace repositories in &lt;code&gt;~/.claude/plugins/marketplaces/&lt;/code&gt;. It was pulling updates to its plugin registry while completing your task. This accounts for two of the three outbound network connections made during the session.&lt;/p&gt;

&lt;p&gt;Both extra network round trips were for Claude's own maintenance, not the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 6: Codex sources a full login shell before every command
&lt;/h2&gt;

&lt;p&gt;Where Claude Code's distinctive behaviour is in the &lt;code&gt;/proc&lt;/code&gt; scan, Codex's is in how it initialises each sandbox shell.&lt;/p&gt;

&lt;p&gt;Before executing any command, Codex reads and sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/profile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Every script in &lt;code&gt;/etc/profile.d/&lt;/code&gt; (bash_completion, locale, flatpak, debuginfod, apps-bin-path, and others)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A full login shell inherits everything configured at the system and user level - including any API keys or tokens you have &lt;code&gt;export&lt;/code&gt;ed in your profile scripts. This is why Codex spawned &lt;code&gt;flatpak&lt;/code&gt;, &lt;code&gt;lsb_release&lt;/code&gt; (five times), &lt;code&gt;getopt&lt;/code&gt;, &lt;code&gt;getconf&lt;/code&gt;, &lt;code&gt;tr&lt;/code&gt;, &lt;code&gt;cut&lt;/code&gt;, and &lt;code&gt;wc&lt;/code&gt; - not as deliberate tool calls, but as side effects of sourcing profile scripts.&lt;/p&gt;

&lt;p&gt;Claude Code does not source profile scripts. It inherits the parent shell's environment via the &lt;code&gt;/proc&lt;/code&gt; scan instead. Two different mechanisms for the same goal - picking up your environment - with different side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 7: Codex connects on port 65535
&lt;/h2&gt;

&lt;p&gt;Both of Codex's API connections used port 65535 - the highest valid TCP port number - rather than the standard HTTPS port 443. The destination IPs (104.18.32.47 and 172.64.155.209) are Cloudflare ranges, so these are routing to OpenAI's API via Cloudflare, but on a non-standard port.&lt;/p&gt;

&lt;p&gt;This is worth noting for anyone running agent workloads in environments with restrictive egress rules. A firewall policy that allows only port 443 outbound will silently break Codex. More broadly, it means traffic analysis tools keyed to port 443 as the signal for "HTTPS to external services" will miss Codex's API calls entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;None of the above is malicious. Claude Code is not stealing your secrets. Codex is not trying to fingerprint your system.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/proc&lt;/code&gt; scan is a reasonable engineering choice for environment variable inheritance. The &lt;code&gt;~/.npmrc&lt;/code&gt; read is an unavoidable side effect of invoking npm. The &lt;code&gt;/etc/passwd&lt;/code&gt; reads are standard Linux user resolution that every process on the machine performs. Sourcing &lt;code&gt;/etc/profile.d/&lt;/code&gt; is how login shells work.&lt;/p&gt;

&lt;p&gt;The problem is not intent. The problem is visibility and scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blast radius problem
&lt;/h2&gt;

&lt;p&gt;When you ask an AI coding agent to edit a file, you are implicitly authorising a surface area you have never seen and cannot predict. In the 17.9 seconds it took Claude Code to add input validation to a route, it had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scanned the environment variables of 752 processes (successfully reading 256, including your browser, Slack, credential store, and other terminal sessions)&lt;/li&gt;
&lt;li&gt;Opened credential files for services unrelated to the task&lt;/li&gt;
&lt;li&gt;Initialised MCP servers with access to your email and calendar&lt;/li&gt;
&lt;li&gt;Made network connections to infrastructure outside the project&lt;/li&gt;
&lt;li&gt;Touched git metadata from repositories above the project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this was requested. All of it happened.&lt;/p&gt;

&lt;p&gt;The individual operations are defensible in isolation. The composition - everything that happens in a single session, across every session you run - is the exposure. In a compromised context (a poisoned file, a malicious tool description, a prompt injection in a comment the agent reads while understanding the codebase), any of these accesses becomes the attack surface.&lt;/p&gt;

&lt;p&gt;This is the same structural problem we identified in &lt;a href="https://dev.to/blog/mcp-servers-new-npm-packages"&gt;MCP server poisoning&lt;/a&gt; and &lt;a href="https://dev.to/blog/clinejection-when-your-ai-tool-installs-another"&gt;the Clinejection attack chain&lt;/a&gt;. The agent has access. Something causes the agent to act. The question is whether anything evaluates what the agent does with that access before it happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visibility gap
&lt;/h2&gt;

&lt;p&gt;Both agents provide some transparency at the tool-call level - you can see "Claude read users.js" and "Codex ran sed". Neither provides visibility at the operation level. You do not see the 752 &lt;code&gt;/proc&lt;/code&gt; attempts, the &lt;code&gt;~/.npmrc&lt;/code&gt; open, the MCP server initialisation, or the parent git config access. These happen below the abstraction layer the agents expose.&lt;/p&gt;

&lt;p&gt;This is not a criticism of the agents. It is a structural property of how they are built. The tool-call interface is designed to show you what the agent did intentionally. The syscall layer is where you see everything else.&lt;/p&gt;

&lt;p&gt;Per-syscall interception is the layer that closes this gap. &lt;a href="https://dev.to/"&gt;grith&lt;/a&gt; sits between the agent process and the kernel, scoring every operation before it executes - not what the agent claims to be doing, but what it actually does. The process census, the credential file opens, the MCP initialisation, the network connections: all of it is visible, all of it is scored, and any of it can be blocked by policy without stopping the agent from completing legitimate work.&lt;/p&gt;

&lt;p&gt;If you want to see this in a real session - including what grith's scoring output looks like against the trace data above - &lt;a href="https://dev.to/"&gt;grith&lt;/a&gt; is live. &lt;a href="https://docs.grith.ai/docs/start/installation" rel="noopener noreferrer"&gt;Install it&lt;/a&gt; and trace your own agent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;All traces were run on Ubuntu 24.04, kernel 6.17.0. &lt;code&gt;strace 6.8&lt;/code&gt; was used with the following flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;strace &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openat,open,connect,execve,getdents64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ttt&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 512 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; raw.log &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; &amp;lt;agent-command&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-f&lt;/code&gt; flag follows all forks and threads, capturing the full subprocess tree. Syscalls with a negative return value (failed operations) are excluded from counts. Agent runtime files (the agent's own node_modules and installation paths) are excluded from file counts but not from sensitive path detection.&lt;/p&gt;

&lt;p&gt;The benchmark project is a minimal Node.js/Express service. The task - "add input validation to the POST /users endpoint" - was identical for both agents in every run.&lt;/p&gt;

&lt;p&gt;Both agents completed the task correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  About grith
&lt;/h2&gt;

&lt;p&gt;This benchmark came out of building &lt;a href="https://grith.ai" rel="noopener noreferrer"&gt;grith&lt;/a&gt;, an open-source security supervisor for AI coding agents. It sits underneath the agent rather than inside it: on Linux, ptrace with a seccomp-BPF pre-filter intercepts every syscall the agent makes and scores it against 18 filters before the kernel executes it. Reads of the kind counted above are scored on the path they touch, and anything ambiguous freezes the process for human review instead of completing silently.&lt;/p&gt;

&lt;p&gt;It is Rust, MPL-2.0, a single static binary, and runs entirely on your own machine. Linux x86_64 and aarch64 today.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/grith-ai/grith" rel="noopener noreferrer"&gt;github.com/grith-ai/grith&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site and docs: &lt;a href="https://grith.ai" rel="noopener noreferrer"&gt;grith.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try it: &lt;code&gt;curl -fsSL https://grith.ai/install | sh&lt;/code&gt;, then &lt;code&gt;grith exec -- claude-code "fix the bug"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://grith.ai/blog/syscall-trace-ai-coding-agents" rel="noopener noreferrer"&gt;grith.ai/blog/syscall-trace-ai-coding-agents&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;strace 6.8 on Linux 6.17.0. The &lt;code&gt;-f&lt;/code&gt; flag follows forks and threads. Only &lt;code&gt;openat&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;execve&lt;/code&gt;, and &lt;code&gt;getdents64&lt;/code&gt; were traced to keep output manageable. Read/write syscalls were excluded; file access is captured via &lt;code&gt;openat&lt;/code&gt; flags instead.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Claude Code 2.1.72 (Anthropic), Codex CLI 0.113.0 (OpenAI). Claude Code was invoked with &lt;code&gt;--dangerously-skip-permissions -p&lt;/code&gt; for non-interactive operation. Codex was invoked with &lt;code&gt;exec --full-auto&lt;/code&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>linux</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
