blog
Mac compressed pages jumped from 5% to 30% because of a log-wait loop
A Claude Code subagent waited on an e2e log with a loop that reads /dev/zero. One bash process grew to 47.7 GB and the Mac compressed page count jumped from 5% to 30% of its limit while RAM still showed 71% available.
- Published
On 2 Nov 2026 the number of pages in my Mac's compressor jumped from 5% to 30% of the limit within minutes, while RAM still showed 71% available. The culprit was a bash process spawned by a Claude Code subagent to wait on a log, and vmmap reported a Physical footprint of 47.7 GB for it, on a 32 GB machine.
This is the third post in a series on the macOS compressed pages limit, after the data.kalloc.1024 kernel panic and the Playwright WebKit case. This time the machine did not panic, but the cause is nothing like the first two.
13:05 to 13:14: compressed pages from 5% to 30%, RAM still 71% available
The machine is a 32 GB Mac Studio on macOS 26, with about 10 Claude Code sessions running in parallel in tmux. A monitoring dashboard reported "compressed pages" going from 5% to 27 to 30% of the limit between roughly 13:05 and 13:14.
That limit is sysctl vm.compressor.pages_compressed_limit, which is 13,192,448 pages on this machine. 85% is where my monitor script asks for a restart, and 100% is a panic, like the one on 31 Oct.
Nothing else looked wrong at the same time:
| Metric | Value |
|---|---|
| Available RAM | 71% |
| Memory pressure | level 1 |
| Compressor size | 6.5 GB |
| IOSurface | 384, flat |
A flat IOSurface count means this was not WebKit, unlike 31 Oct. The only odd line in the top-memory list was bash 5.3G.
The culprit: a subagent's log-wait loop
A subagent in one of the project sessions was waiting for the e2e suite to finish, so it wrote this loop:
for i in $(seq 1 55); do
grep -qE "passed" e2e-full.log && break
read -t 9 < /dev/zero || true
done
It meant to sleep 9 seconds between log checks. It avoided sleep because the Claude Code harness blocks a sleep that runs in the foreground, so the model invented another way to wait.
Why bash reached 47.7 GB while RAM stayed free
read reads until it sees a newline. /dev/zero only returns zero bytes and never a newline, so bash keeps collecting zero bytes into its buffer for the whole 9 seconds of each iteration, until -t 9 cuts it off.
vmmap --summary on that bash process reported a Physical footprint of 47.7 GB. RSS was only 5.3 GB, matching the bash 5.3G line in the top-memory list, because the rest had been compressed.
Pages full of zeros compress extremely well. That is why RAM still looked free, the compressor only grew to 6.5 GB, and only the page count in the compressor shot up.
Tracing the process with ps and vmmap
When compressed pages climb while RAM and IOSurface look normal, find the child bash of the claude process:
ps -axo pid,ppid,rss,etime,command -m | head
vmmap --summary <pid> | grep "Physical footprint"
The command line of that bash shows read -t 9 < /dev/zero right away. The -m flag sorts by memory, and etime says how long it has been running.
Killing it was blocked: wait for the loop to end
A kill on the bash process was blocked by Claude Code's auto mode, with the reason "Interfere With Workloads".
At 13:14 the loop ended and compressed pages fell back to 7%. From 13:14 to 13:19 was the subagent's last wait: compressed pages rose to 15%, then fell to 6%.
The fix: a hook that blocks reads from /dev/zero, not a prompt instruction
Telling the subagent "don't wait with /dev/zero" is not enough, because an instruction in a prompt does not stop an agent that is looking for a way around a rule. A hook does, subagents included.
A PreToolUse hook named ram-gate now always denies Bash commands that read /dev/zero or /dev/urandom into read or into a variable:
read … < /dev/zerocat /dev/zero | read$(< /dev/zero)$(cat /dev/urandom)
Bounded commands still pass: head -c N /dev/urandom, dd … count=N.
The cost: the hook matches strings in the command, so the same text inside a command as plain text (a grep pattern, a heredoc) is blocked by mistake.
How to wait properly
- Run the long command in the background and wait for the notification when it exits.
- Or use
Monitorwith anuntilloop that checks the condition.
Lessons: free RAM lies, hard rules belong in a hook
- A "free RAM" figure lies when the data is all zeros. Watch the compressed page count against its limit, not just the compressor size.
- The same rising page count can come from the kernel (IOSurface in the 31 Oct case) or from a user process.
psplusvmmaptells the two apart in seconds. - Agents invent ways around rules: with
sleepblocked, it becameread < /dev/zero. - A rule that must be enforced belongs in a hook, because a hook stops subagents and a prompt instruction does not.
Related posts
all posts →- A Mac with 65% RAM free nearly panicked: the compressor, IOSurface and Playwright's WebKitA 32 GB Mac Studio panicked with 100% of compressed pages limit, then came close to a second panic that evening while RAM still showed 65% free. The page count in the compressor had filled up, and the culprit was IOSurface that Playwright WebKit left in the kernel.read more →
- Kernel panic data.kalloc.1024 on a Mac running many Claude Code sessionsA 32 GB Mac Studio running about 20 Claude Code sessions rebooted itself 5 times because the kernel zone data.kalloc.1024 leaked to about 20 GB. How to watch it with zprint, why the npm install leaks faster, and the 3 steps that stopped it.read more →
- REA: letting an agent reverse engineer software, then testing the oracle tooREA lets a coding agent read a program that has no source, down to machine code, then rebuild that feature in its own code. Whether the rebuild is right is up to the oracle, and the oracle is code too, with its own bugs.read more →
- Top 10 Claude Code skills by GitHub stars: what each one does and which ones overlapThe 10 Claude Code skill sets with the most GitHub stars on 29 Oct 2026: Superpowers, Matt Pocock Skills, Karpathy Skills, Anthropic Skills, Ponytail, UI UX Pro Max, Graphify, Caveman, Agent Skills, Taste Skill. Each one with its install command, what it does and where it overlaps with another set.read more →