Codex swept my whole disk for credentials. grith froze every real one
During a DNS bug debugging task, the Codex AI agent began scanning the entire filesystem for credentials without instruction. The grith security proxy intercepted all such accesses at the OS level, freezing and queuing every real credential store for human approval. This article details the technical analysis, including process tracing, audit logs, and grith's response, and discusses residual risks.
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.
esc to close
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.
There were two processes involved. One was Codex itself, doing its normal work - SQLite scratch files, the occasional network listener, nl on source files. The other was a child process it spawned, and that child did exactly one thing for four and a half minutes straight:
13:13:08 child spawned 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 child exits
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 the child process 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.
esc to close
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.
esc to close
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.
Wait - is this even Codex, or is it 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 the sweep's reads, but there was no ProcessSpawn record in the audit log for the enumerating process. We could see it live in pstree, but the exec that created it never landed in audit.db. Enforcement was never in doubt - the reads were caught and queued - but the forensic link that answers "what command kicked this off" was missing from the durable trail. We are fixing the coverage gap. 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 w
[truncated for AI cost control]