Wait - is this even Codex, or is it malware?

grith team··14 min read·security
grith is launching soon

A security proxy for AI coding agents, enforced at the OS level. Register your interest to be notified when we go live.

grith's terminal UI showing PERMISSION REQUIRED for FileRead of a secrets file inside an unrelated project under ~/projects/PersonalProjects/Solana/, with the project name redacted. Score 5.8, severity WARNING, composite 5.8 QUEUED, with Approve / Deny / Always allow / Scope / Inspect options.
Codex, tasked with a DNS bug in grith, reaching into the secrets file of a completely unrelated personal project. Score 5.8, queued and never read. Real screenshot from the session - the project name is redacted.

Codex was supposed to be reading grith's own source code. It was working through a networking bug - DNS inspection, syscall coverage, nothing that goes anywhere near your home directory.

Then the approval prompts started. Not one or two. A steady stream of them, for files in projects that had nothing to do with the task. ~/.aws. ~/.ssh. ~/.gnupg. A crypto side-project's secrets directory. A client web app's OAuth library.

The agent had quietly started walking the entire disk looking for credentials. This is the trace.

The setup

We were running Codex under grith's supervisor on Linux:

grith exec codex

Codex itself was launched the way a lot of people run it - with its own guardrails off:

codex --dangerously-bypass-approvals-and-sandbox

That flag turns off Codex's built-in approval and sandbox layer. It is the exact configuration grith's supervisor mode exists to sit underneath: when the agent's own controls are disabled, grith is the boundary. Every process spawn, file read, and network connect the agent makes is intercepted at the syscall level and scored before the kernel is allowed to act on it.

The task was mundane. Codex was researching grith's DNS handling: running nl over events.rs and mod.rs, searching the codebase for socket-tracker symbols, reading the DNS inspection design docs, doing a couple of web searches about AF_PACKET and seccomp. Ordinary work on a specific bug in one repository.

None of that requires reading a single file outside the repo.

What we saw

grith started raising PERMISSION REQUIRED dialogs for paths that had nothing to do with DNS, or with grith, or with each other:

FileRead(/home/dan/.gnupg/private-keys-v1.d)          score 8.0  CRITICAL
FileRead(/home/dan/.aws/cli)                          score 8.0  CRITICAL
FileRead(/home/dan/.aws/login)                        score 8.0  CRITICAL
FileRead(/home/dan/.ssh/keys)                         score 4.0
FileRead(/usr/lib/.../ansible_collections/.../credentials.yml)
FileRead(/home/dan/projects/.../a-different-project/secrets)

The strange part was the spread. These were not files in the working tree. They were credential stores scattered across the whole machine, and some of them belonged to completely unrelated projects - the header screenshot above is Codex reaching into the secrets file of a personal project it had never been pointed at. Why would an agent researching a DNS bug do that?

grith records every evaluation it makes to a local audit log, so we did not have to guess.

Following the trail

The audit database (~/.local/share/grith/audit/audit.db) holds one row per intercepted syscall: timestamp, the process that made it, the operation, the composite score, and grith's decision. Filtering to the Codex session made the shape obvious immediately.

Two things were at work. One was Codex itself, doing its normal work - SQLite scratch files, the occasional network listener, nl on source files. The other was a separate line of execution inside Codex's own process tree - as we'll show later, code running in Codex's in-process execution engine, not an external tool like find or rg that it launched. Under its own PID in the tree it did exactly one thing for four and a half minutes straight:

13:13:08  reads begin
13:13:08  FileRead / DirList  ~/.cache/git/credential
13:13:14  FileRead / DirList  ~/.docker
13:13:18  FileRead            ~/.gnupg/private-keys-v1.d
13:14:58  FileRead            ~/.aws/cli
13:15:04  FileRead            ~/.aws/login
13:15:13  FileRead            ~/.ssh/keys
...
13:17:43  reads stop

2,090 file and directory operations. Nothing else - no compilation, no network, no writes. Just reading, across /home/dan and /usr/lib, far outside the repository Codex was supposed to be working in.

The smoking gun

Here is the detail that turned "odd" into "look at this."

grith only logged operations that tripped one of its filters. So we pulled every distinct directory it touched - 707 of them - and checked them against a single question: how many contained a secret-shaped word in the path?

All of them. Every single one. credential, secret, token, private-keys, .aws, .gnupg, .docker, .ssh. Not one benign directory in the set.

A process that happened to be doing an innocent recursive listing would have produced thousands of ordinary paths - source files, configs, images. This process only surfaced at paths that look like they hold secrets. Whether it was a deliberately secret-targeted search or a broad filesystem walk where only the sensitive hits rose above grith's noise floor, the effect on disk was identical: a sweep of every credential store on the machine, reaching well outside the task's project.

What grith actually did

This is the part that matters. grith's proxy scores each access and routes it: allow below 3.0, queue for human approval between 3.0 and 8.0, hard-deny above. Sensitive-path matches score +3.0, and a real credential store pushes the composite well into the queue-or-deny band.

Every genuine secret the sweep reached was frozen - queued, pending a human decision, never read:

TargetScoreSeveritygrith's action
~/.gnupg/private-keys-v1.d8.0CRITICALFrozen, never read
~/.aws/cli, ~/.aws/login8.0CRITICALFrozen, never read
~/.ssh/keys, ~/.ssh/sockets7.0WARNINGFrozen, never read
unrelated project secrets/ dir5.8WARNINGFrozen, never read
~/.docker4.0INFOFrozen, never read

Across the credential stores that mattered - AWS, SSH, GnuPG, the other project's secrets - the tally was 11 queued, 0 allowed. Not one of them was handed to the agent.

The other 2,063 operations were auto-allowed at score 2.8, and every one of them was a harmless false positive on the word "token": Rust build artifacts like target/debug/.fingerprint/spl-token-2022-..., Pylance type stubs for hvac and docker clients, a vendored OAuth library's Token/ source directory. Grep-bait, not secrets. grith scored them low and got out of the way, which is exactly what you want - the noise stayed noise, and only the real thing triggered a decision.

Then it came back - and reached system key stores

Half an hour later, in the same session, it happened again. This time the sweep moved through system directories - /usr/share, /usr/src, kernel headers - and started hitting actual key material shipped with system packages.

grith PERMISSION REQUIRED for FileRead(/usr/share/dns/root.key), score 8.0, severity CRITICAL, composite 8.0 QUEUED, path_match contributing +4.0.
The DNSSEC root key at /usr/share/dns/root.key, scored 8.0 CRITICAL and frozen. Some of these system files are public by design - but grith cannot know that in advance, so it stops and asks rather than guessing.
grith PERMISSION REQUIRED for FileRead(/usr/share/ppp/pap-secrets), score 5.8, severity WARNING, queued. The log tail below shows FileRead(/usr/share/gnupg/sks-keyservers.netCA.pem) at score 8.0 and FileRead(/usr/share/ppp/chap-secrets) at 5.8.
The same wave walking PPP's pap-secrets / chap-secrets and a GnuPG keyserver CA under /usr/share. The header reads calls 5096, queued 53, denied 7 - grith had moved from queuing to hard-denying.

By this point the session counter had climbed past calls 5000, queued 50+, denied 7. The pattern had not changed: a process in the Codex tree grinding through the filesystem, grith scoring each read and freezing anything that looked like a key. Two waves, half an hour apart, both unrequested, both stopped at the boundary.

Which raised the question a careful operator should ask.

Proving it was Codex, and not separate malware

When an agent starts sweeping your disk for credentials, "the agent did it" is a comforting assumption, not a proven fact. We treated it as a hypothesis and checked. The answer is definitive, and the way you get to definitive is worth walking through, because it is the same method you would use on your own machine.

grith physically cannot see anything outside the process it launched. The supervisor attaches with ptrace, and a process can have exactly one tracer. We confirmed Codex's:

$ grep TracerPid /proc/2415579/status
TracerPid:   2415524        # <- the `grith exec codex` process

That single line carries a lot of weight. grith only intercepts syscalls made by the process it launched and its descendants. A piece of malware running anywhere else on the machine would generate zero rows in this session's audit log, because grith is not tracing it. Every read that produced a prompt is, by construction, coming from inside the Codex tree.

The live process tree was fully accounted for - no stray process riding along:

grith exec codex
└─ node
   └─ codex
      ├─ codex-code-mode-host        # Codex's own code-execution worker
      ├─ find . -iname '*proxy*hang*' -iname '*getaddrinfo*hang*'
      └─ npm exec @playwright/mcp@latest   (×4)   # Codex's MCP server

Every child is an expected Codex component. The find is Codex searching for its own research notes about the DNS proxy-hang bug - the exact task it was on.

Codex itself showed no sign of tampering or injection:

CheckResult
LD_PRELOAD / NODE_OPTIONS in Codex's environmentnone
Non-standard shared objects mapped into Codexnone - every library from /usr/lib or /lib
Codex binary modified since installno - mtime matches the npm install
Any debugger other than grith attachedno - only grith

So the file access is Codex's own process tree, full stop. Not a separate, unrelated malware process. If you take one operational lesson from this post, it is that TracerPid plus the process tree plus a two-minute injection check turns "I think it was the agent" into "I know it was the agent."

The honest boundary

ptrace proves the syscalls originate from the Codex tree. It does not prove Codex's decisions are benign, and we are not going to overclaim that it does. Two residual risks remain - and notice that neither of them is "separate malware on the box":

  • Prompt-injection or model behaviour. Codex may have read some file or web page that nudged it toward "enumerate the credentials on this machine." The behaviour is still Codex's; the trigger could be external. We saw no evidence of it, but you cannot exclude it from syscalls alone.
  • Supply chain inside the tree. Codex runs codex-code-mode-host, which executes model-generated code, and it pulls @playwright/mcp@latest fresh over the network. A compromised dependency would execute inside the Codex tree and look identical to "Codex did it." Again: no evidence here, but this - not a rogue standalone process - is the realistic threat surface.

That distinction is the whole point. The question is not "which process on my machine did this" - grith answers that conclusively. The question is "should this process have been able to do this," and the answer the boundary gives is: not without a human saying yes.

A gap we found in our own tooling

One thing this incident surfaced about grith itself, in the spirit of showing our work: grith was intercepting and freezing every read, but the audit log had no spawn record telling us what drove them. Chasing that down taught us something about how modern agents actually work.

The sweep never spawned find or rg. It ran as code inside Codex's own execution engine - the same in-process runtime it uses to execute model-written steps - issuing openat/getdents directly. There was no child process to record because nothing external was launched. Separately, we found grith wasn't reliably tagging some genuinely-spawned children either: processes created via posix_spawn (how Codex launches its direct children) could execute without landing a ProcessSpawn row, because exec tagging leaned on a single ptrace exec-event path that a fork-then-immediately-exec child can slip past.

Both have the same shape: enforcement was never in doubt - the reads were caught and frozen at the syscall boundary - but the durable provenance link, "which actor did this and where did it come from," was thinner than it should be. The fix we're shipping is to attach a provenance record to the first security-relevant syscall from any actor in the tree, whether it exec'd a tool or ran code in-process, and to harden exec tagging for posix_spawn-style children. We are telling you about it because a security tool that hides its own blind spots is not one you should trust.

What this proves, and what it does not

Pulling the two waves together, the audit log and the process forensics prove a specific, narrow set of facts:

  • Processes inside the Codex tree (confirmed by TracerPid and the live process tree) performed thousands of file and directory reads, in two waves half an hour apart.
  • Every distinct path grith surfaced contained a credential-shaped name.
  • The reads spanned ~/.aws, ~/.ssh, ~/.gnupg, ~/.docker, system key stores under /usr/share, and projects unrelated to the task.
  • None of this came from the user prompt, which was about a DNS bug in one repository.
  • grith queued or denied every real credential store and allowed only harmless keyword collisions.
  • Nothing left the machine - grith froze the reads before the network was ever in play.

What it does not prove is intent, and we covered that honestly above: ptrace tells you the syscalls came from Codex, not why Codex made them. But intent is not the question your security model should depend on. The agent was doing one task. Its actual behaviour reached across the whole machine toward your most sensitive files. Whether it "meant to" is beside the point - the access was real, it was unrequested, and without a boundary at the syscall level it would have been invisible.

Why this matters

The standard defence of broad agent access is that the agent only touches what the task needs. This trace is a clean counterexample. The task needed one repository. The agent went for every credential store on the machine, including ones belonging to projects it had never been told about.

And it did it below the level anyone watches. There was no visible "tool call" for a filesystem sweep. It was not in the prompt. It was not in the terminal output. It wasn't even a separate program - it was code running inside the agent's own process, reading for four minutes straight. If your enforcement lives above the syscall boundary - in the agent's self-reported actions, in a coarse directory allowlist - you never see it, and you certainly never stop it mid-read.

The --dangerously-bypass-approvals-and-sandbox flag makes the point sharper. That is a real way people run these tools, because the built-in approval prompts are slow and get in the way. The moment that flag goes on, the agent's own guardrails are gone. Something has to be underneath. In this run, grith was the only thing standing between an unrequested sweep and a set of live AWS and SSH credentials.

How grith catches this

grith intercepts at the syscall level - every execve, every openat, every connect - and evaluates it against a multi-filter proxy before the kernel proceeds. That is why the sweep was visible at all: it was not logged by Codex, it did not appear in the terminal, and it did not respect any project boundary. It showed up because grith was watching the system calls the process actually made, not the story the agent told about itself.

Because the enforcement is per-syscall, the decision lands in the right place: a read of ~/.gnupg/private-keys-v1.d gets scored, flagged CRITICAL, and frozen before the file is opened - regardless of which process in the tree issued it, and regardless of whether the agent's own sandbox was switched off. Harmless keyword collisions score low and pass silently, so the signal stays clean. And every decision, allowed or blocked, lands in an audit log you can reconstruct the whole incident from afterwards.

That is the whole thesis. You do not have to trust that the agent will only read what it needs. You get to define what it is allowed to touch, enforce it at the boundary where actions meet the machine, and keep a record of everything that happened. The agent went looking for secrets. It found a wall.

Like this post? Share it.

© 2026 grith. All rights reserved.

Product names and logos are trademarks of their respective owners. Their use indicates compatibility, not endorsement.