A security proxy for AI coding agents, enforced at the OS level. Register your interest to be notified when we go live.
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:
| Target | Score | Severity | grith's action |
|---|---|---|---|
~/.gnupg/private-keys-v1.d |
8.0 | CRITICAL | Frozen, never read |
~/.aws/cli , ~/.aws/login |
8.0 | CRITICAL | Frozen, never read |
~/.ssh/keys , ~/.ssh/sockets |
7.0 | WARNING | Frozen, never read |
unrelated project secrets/ dir |
5.8 | WARNING | Frozen, never read |
~/.docker |
4.0 | INFO | Frozen, 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.
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:
| Check | Result |
|---|---|
LD_PRELOAD / NODE_OPTIONS in Codex's environment |
none |
| Non-standard shared objects mapped into Codex | none - every library from /usr/lib or /lib |
| Codex binary modified since install | no - mtime matches the npm install |
| Any debugger other than grith attached | no - 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 runscodex-code-mode-host
, which executes model-generated code, and it pulls@playwright/mcp@latest
fresh over the network. A compromised dependency would executeinsidethe 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 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. A child process spawned, ran for four minutes, and exited. 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.