Your AI coding agent just ran make -j, kicked off the full test suite in parallel, and started a cargo build — all at once, on the same machine, without ever asking whether that machine could take it. It can't feel the fans spin up. It doesn't know the load average just hit 40 on an 8-core laptop. It sees "run command," so it runs the command.
Mine did exactly this last week. The MacBook got hot enough to fry an egg, the fans screamed, and everything — including the agent — slowed to a crawl. So I built heatsink: a small guard that sits between an AI agent and your shell, checks the machine's actual load and thermals, and right-sizes heavy parallel commands before they run. It's open source (MIT), pure bash, zero dependencies. This is the story of why, and what it taught me.
The problem: agents optimize for speed they can't pay for
A human developer running a slow build feels the heat, hears the fan, and backs off — maybe drops -j16 to -j4, maybe waits for the other job to finish. That feedback loop is physical. An agent has none of it.
Worse, agents parallelize aggressively, because on paper more parallelism is faster. Three make -j invocations, a vitest run with max threads, cargo build with all cores — each one individually reasonable, all of them together an oversubscription pile-up. The scheduler thrashes, everything gets slower, and the laptop turns into a space heater.
And then there are the leftovers. When I finally went looking, the real culprit wasn't even the active builds — it was ten orphaned yes processes, each pinning a core, parented to init, spinning forever because some earlier run spawned them and never reaped them. The agent had moved on. The CPU hadn't.
The idea: a guard that feels the heat for the agent
heatsink gives the agent the physical feedback it's missing. Before a heavy command runs, it asks two questions the machine can actually answer:
- How loaded am I right now? Load average over core count.
- Am I thermally throttling? macOS
pmset -g therm/ Linux/sys/class/thermal.
Then it acts on a simple policy:
ratio = load1 / cores
ratio < 0.9 → ok, run as-is
ratio ≥ 0.9 → throttle: reduce the parallelism
ratio ≥ 2.0 → deny: machine is already buried
thermal pressure escalates one level.
heatsink maps load1 ÷ cores to an action — run, throttle, or deny. Thermal pressure bumps you one zone hotter.
"Throttle" doesn't mean block — it means rewrite. make -j16 becomes make -j4 when you're already cooked. cargo build -j12, pytest -n auto, parallel rspec — each gets its worker count reduced to what the machine can actually sustain, then runs. The agent keeps working; it just stops setting the house on fire.
Setting
-jor a worker pool by hand? heatsink picks a safe number at runtime, but when you're choosing parallelism deliberately, plug your core count and target load into the Throughput & Concurrency Calculator to find the point that saturates the machine without tipping it into thrash.
Two rules keep it safe:
- Reduce-only. heatsink never raises parallelism, and never adds
-jto a baremakethat didn't ask for one. It can only walk you back from an over-commitment. - Fail open. If any check errors, times out, or the tool isn't sure — the command runs unmodified. A guard that wedges your agent is worse than no guard. This is non-negotiable, and it's why heatsink needs no
sudoand no root-level thermometer.
It also finds those orphaned burners:
heatsink doctor # why is my machine hot? load, top burners, orphans
heatsink reap # list orphaned CPU-burners (report only)
heatsink reap --kill # terminate them — explicit opt-in, never automatic
reap is deliberately conservative: it only ever considers your own processes, parented to init, that either match a known burner profile or have been pegging a core for over an hour. And it never sends a signal without the explicit --kill flag.
Wiring it into the agent
The whole point is that this happens without the agent thinking about it. For Claude Code, heatsink ships as a plugin that installs itself into the PreToolUse hook — every shell command the agent proposes passes through the guard first, and gets transparently right-sized:
/plugin marketplace add sundarshahi/heatsink
/plugin install heatsink@heatsink
There are adapters for Cursor and Codex too, and a generic heatsink wrap -- <cmd> that works in any shell or CI step. Or, for a zero-install look at your own machine right now:
npx @sundarshahi/heatsink doctor
What building it taught me
Two bugs from shipping heatsink were more instructive than the tool itself.
The tests that weren't testing anything
heatsink has 76 tests in bats. They passed on my Mac. Then I pushed, and the Linux CI leg went red on two of them — which was confusing, because the same tests were green locally.
The root cause was worse than two failing tests. My assertions were written as bare [[ ... ]] expressions. On the macOS bats build, a failed bare [[ ]] didn't abort the test — only the last assertion in each test actually decided pass/fail. Roughly thirty assertions across the suite were silently advisory. Linux bats aborts correctly, which is exactly why it caught what my machine hid.
And one of the assertions that had been quietly not-running was a safety check: it asserted that a process literally named kyes (a substring of yes) must not be flagged as a reap target. With the assertion vacuous, a name-substring near-miss could have become a kill candidate. The fix was one character of discipline per assertion — [[ ... ]] || return 1 — and then fixing the two real bugs it exposed. The lesson: a test that can't fail is worse than no test, because it reads as coverage.
The install that only broke after publishing
heatsink worked perfectly from git clone and make install. I published it to npm, ran npx @sundarshahi/heatsink doctor from a clean machine — and it fell over, unable to find its own library files.
npm installs a CLI by symlinking the binary into node_modules/.bin/. My script resolved its own location by following that symlink to find the sibling lib/ directory — but it treated the symlink's relative target as relative to the current directory instead of to the link itself. make install used a layout that never exercised the bug; npm's layout hit it immediately. No test caught it because every test ran the binary in place, never through a symlink.
The fix was the standard robust symlink-resolution loop — resolve each hop against the link's own directory. But the real takeaway: your install path is part of your software. "Works on my machine" and "works from git clone" are not the same claim as "works the way a stranger will actually install it."
The takeaway
AI agents are getting very good at deciding what to run. They are still blind to the physical cost of running it on your actual hardware. Until that feedback loop is built in, a thin guard that feels the heat on the agent's behalf is a cheap, boring, effective fix — and boring is a compliment.
If your laptop fans have become your pair-programming soundtrack, heatsink is one command away:
npx @sundarshahi/heatsink doctor
- Source (MIT): github.com/sundarshahi/heatsink
- Package: npmjs.com/package/@sundarshahi/heatsink
It fails open, needs no sudo, and only ever walks your parallelism down. Feedback and pull requests welcome.
Runaway CPU is one way agents surprise you in production. If you're operationalizing AI coding agents more broadly, the Agent Reliability Scorecard walks through the rest of the failure modes worth guarding against.