We asked Claude Code to add input validation to a single route handler. The task required editing one file. Roughly 20 lines of code.
Here is what happened before it wrote a single character.
We built a standardised benchmark: a small Node.js/Express user management service with a POST /users
endpoint that was deliberately missing input validation. The task was identical for both agents: add validation for name
, email
, and password
, return 400 with a descriptive error for each failure case.
We ran each agent under strace -f
, recording every openat
, connect
, execve
, and getdents64
call made by the agent process and every subprocess it spawned1. 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.
Agents tested: Claude Code 2.1.72 and Codex CLI 0.113.02.
The benchmark project and analysis scripts are published on GitHub if you want to run this yourself.
| Claude Code | Codex | |
|---|---|---|
| Task duration | 17.9s | 42.2s |
| Files read (unique) | ||
| 2,779 | ||
| 303 | ||
| Files written | 10 | 20 |
| Files in the project | 7 | 4 |
| Files outside the project | 2,782 | 319 |
| Directory scans | 368 | 750 |
| Unique subprocesses spawned | 14 | 25 |
Claude Code opened 2,779 unique files in the process of editing one. Codex opened 303.
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.
During the session, Claude Code attempted to open /proc/<pid>/environ
for 752 distinct process IDs - every process visible in /proc
, from PID 1 upward. Most kernel and root-owned processes returned EACCES
, but 256 succeeded - 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 /proc
systematically on startup to inherit the parent shell's environment variables.
The mechanism is legitimate. Shells export environment variables - PATH
, NODE_ENV
, API keys you have exported in your .zshrc
- and a terminal-launched process cannot read them any other way on Linux. So Claude Code walks
/proc
to find them.
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:
| Process | Instances | What it is |
|---|---|---|
| Firefox | 56 | Browser (tabs, extensions, GPU process) |
| VS Code | 15 | Editor and extension hosts |
| Zoom | 13 | Video conferencing (webview hosts) |
| Slack | 6 | Messaging |
| Claude Code | 7 | Other Claude sessions |
| Codex | 4 | Other Codex sessions |
| gnome-keyring-daemon | 3 | System credential store |
| bash / tmux | 27 | Terminal sessions |
| node | 4 | Node.js processes |
| DBeaver (Java) | 1 | Database client |
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 /proc/<pid>/environ
, which contains the DBUS_SESSION_BUS_ADDRESS
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.
It also read /proc/<pid>/stat
, /proc/<pid>/status
, and /proc/<pid>/cmdline
for processes starting from PID 1 - process names, states, and full command lines for the entire running process table.
Codex did not do any of this. Zero /proc/*/environ
reads in either run.
Neither agent needed any of the following to add input validation to a route. Both read them anyway.
| Path | Claude Code | Codex | Why |
|---|---|---|---|
~/.gitconfig |
|||
| x49 | x47 | Each of the 16-18 git subprocesses resolves the global config independently |
|
/etc/passwd |
|||
| x20 | x16 | Every subprocess resolves the current user via getpwuid() on startup |
|
~/.npmrc |
|||
| x1 | x1 | npm registry auth token | |
~/.ssh/config |
|||
| x2 | - | SSH config, read during git remote operations | |
~/.ssh/known_hosts |
|||
| x4 | - | SSH host verification |
The ~/.gitconfig
and /etc/passwd
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.
The ~/.npmrc
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.
After the session completed, we found two log files written outside the project:
~/.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
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.
The network trace corroborates this: one of Claude Code's outbound connections was to a Google Cloud IP (137.66.149.34.bc.googleusercontent.com:443
), which does not appear in Codex's trace.
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.
The benchmark project was a subdirectory of a larger git repository. Claude Code found and read the parent project's .git/HEAD
and .git/config
.
This is standard git behaviour - git
walks up the directory tree to find the nearest .git
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.
During the session, Claude Code acquired git locks and wrote ORIG_HEAD.lock
files to two plugin marketplace repositories in ~/.claude/plugins/marketplaces/
. 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.
Both extra network round trips were for Claude's own maintenance, not the task.
Where Claude Code's distinctive behaviour is in the /proc
scan, Codex's is in how it initialises each sandbox shell.
Before executing any command, Codex reads and sources:
/etc/profile
/etc/profile.d/
(bash_completion, locale, flatpak, debuginfod, apps-bin-path, and others)A full login shell inherits everything configured at the system and user level - including any API keys or tokens you have export
ed in your profile scripts. This is why Codex spawned flatpak
, lsb_release
(five times), getopt
, getconf
, tr
, cut
, and wc
- not as deliberate tool calls, but as side effects of sourcing profile scripts.
Claude Code does not source profile scripts. It inherits the parent shell's environment via the /proc
scan instead. Two different mechanisms for the same goal - picking up your environment - with different side effects.
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.
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.
None of the above is malicious. Claude Code is not stealing your secrets. Codex is not trying to fingerprint your system.
The /proc
scan is a reasonable engineering choice for environment variable inheritance. The ~/.npmrc
read is an unavoidable side effect of invoking npm. The /etc/passwd
reads are standard Linux user resolution that every process on the machine performs. Sourcing /etc/profile.d/
is how login shells work.
The problem is not intent. The problem is visibility and scope.
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:
None of this was requested. All of it happened.
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.
This is the same structural problem we identified in MCP server poisoning and the Clinejection attack chain. 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.
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 /proc
attempts, the ~/.npmrc
open, the MCP server initialisation, or the parent git config access. These happen below the abstraction layer the agents expose.
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.
Per-syscall interception is the layer that closes this gap. grith 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.
If you want to see this in a real session - including what grith's scoring output looks like against the trace data above - grith is live. Install it and trace your own agent.
All traces were run on Ubuntu 24.04, kernel 6.17.0. strace 6.8
was used with the following flags:
strace -f \
-e trace=openat,open,connect,execve,getdents64 \
-e signal=none \
-ttt -s 512 \
-o raw.log \
-- <agent-command>
The -f
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.
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.
Both agents completed the task correctly.
This benchmark came out of building grith, 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.
It is Rust, MPL-2.0, a single static binary, and runs entirely on your own machine. Linux x86_64 and aarch64 today.
curl -fsSL https://grith.ai/install | sh
, then grith exec -- claude-code "fix the bug"
Originally published at grith.ai/blog/syscall-trace-ai-coding-agents.
strace 6.8 on Linux 6.17.0. The -f
flag follows forks and threads. Only openat
, open
, connect
, execve
, and getdents64
were traced to keep output manageable. Read/write syscalls were excluded; file access is captured via openat
flags instead. ↩
Claude Code 2.1.72 (Anthropic), Codex CLI 0.113.0 (OpenAI). Claude Code was invoked with --dangerously-skip-permissions -p
for non-interactive operation. Codex was invoked with exec --full-auto
. ↩