Note
Chrome's code_sign_clone: 180 GB by du, about 6 GB for real
My Mac's data volume hit 96% full. Looking for what was taking the space, I found a folder I had never heard of:
/private/var/folders/<hash>/X/com.google.Chrome.code_sign_clonedu said it held 180 GB: 127 subfolders named code_sign_clone.XXXXXX, 1.4 GB each, the size of Google Chrome.app itself. After I deleted them, the disk gave back about 6 GB. This note covers why those two numbers differ, what the folder is, and what filled it.
du is not the space you get back
The clones are copy-on-write: they share blocks with the original and with each other. du counts the shared blocks again for every clone, which is how 127 clones add up to 180 GB.
To see the effect, I made ten cp -c clones of a 200 MB file. du reported 2.1 GB, and free space did not change. On the real folder, df showed used space on the data volume going from 192 GiB to 186 GiB, and free space from 10 GiB to 16 GiB, once I had deleted the old clones.
du on a folder of APFS clones is not what you get back by deleting it. Use df, or the system's storage view, to measure what a cleanup really frees.Treat 6 GiB as an estimate: the two df readings were minutes apart on a machine in use. Clones of the same Chrome version also share blocks with each other, so the real cost depends on how many different Chrome versions the clones span, not on how many clones there are. Either way the clones were not what filled the disk.
What code_sign_clone is
Every time Chrome starts on macOS, it clones its own app bundle into /var/folders. This is Chromium's CodeSignCloneManager, and its source says how it works:
- The clone is made with
clonefile, and its main executable is replaced by a hard link to the original. It keeps the files a running Chrome is using available after a staged update, and keeps the code signature valid. - On a normal exit, a small helper process deletes the clone.
- If Chrome is killed or crashes, that cleanup never runs, and nothing removes the clone later. The source has an open TODO for stale clones, and a comment says the folder is cleared at boot.
- Chrome for Testing is skipped: it does not auto-update, and the feature exists for updates.
I confirmed two things on my machine: the Chrome build contains CodeSignCloneManager, and the main executable in the clone of my running Chrome has the same inode as the one in /Applications. Cốc Cốc, another Chromium browser I have, has the same kind of folder.
What left so many behind
My first guess was Chrome's auto-update. It was not: updating does not create a clone, only starting Chrome does. And the clones appeared in bursts of a few per minute (27 Oct 12:54 to 13:16, 27 Oct 17:03 to 17:48, 28 Oct 10:07 to 10:16), which is not how anyone opens a browser by hand.
The times matched sessions of an AI coding agent. While checking a UI, it wrote throwaway scripts that took screenshots and tested hover states. Each one started the real Chrome headless and ended with chrome.kill() or pkill -f. One session did that 47 times and 12 times respectively. A screenshot skill on the machine does the same and finishes with SIGKILL. I matched this by time and script contents, not clone by clone.
To check, I launched headless Chrome with a throwaway profile, waited, then killed it (or closed it), and counted clones before, during and after:
| Binary | How it ended | Clones before / during / after | Exited within 4 s? |
|---|---|---|---|
| Chrome for Testing | SIGTERM | 1 / 1 / 1 | yes |
| Chrome for Testing | SIGKILL | 1 / 1 / 1 | yes |
| Chrome | SIGTERM | 1 / 2 / 2 | yes |
| Chrome | SIGKILL | 2 / 3 / 3 | yes |
| Chrome | Browser.close | 1 / 2 / 1 | yes |
The leading 1 in the first four rows is a leftover from an earlier run; in the last row it is the clone of my everyday Chrome, which was open the whole time.
Real Chrome leaves a clone on SIGTERM as well as SIGKILL, so swapping one signal for the other does not help. Chrome for Testing leaves none, which matches the source. Closing real Chrome through the DevTools protocol is different: with Browser.close the process exited after about 90 ms, and the new clone was gone within a second. I repeated it three more times with the same result. That is a normal exit, so the cleanup helper runs.
The fix
Quit Chrome completely (Cmd+Q) and delete the folder, and Chrome will make a new one next time. Do not do it while Chrome is open, because the running browser's own clone lives there:
rm -rf /private/var/folders/<hash>/X/com.google.Chrome.code_sign_cloneThen stop scripts from using the real Chrome. Use Chrome for Testing. If you have agent-browser, it already installed one under ~/.agent-browser/browsers/chrome-<version>/. The folder carries a version number, so pick the newest instead of hardcoding it:
CHROME="$(ls -d ~/.agent-browser/browsers/chrome-*/ | sort -V | tail -1)Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"If a script really has to use the real Chrome, end it with Browser.close instead of killing it.
Setting CHROME_PATH is not enough on its own. The scripts an agent writes on the fly hardcode the real path and never read it, and tools that do read it fall back silently to the real Chrome when the path stops existing, for example after a version bump. So the rule has to be where the agent reads it, such as CLAUDE.md:
## Headless browser
- Never spawn `/Applications/Google Chrome.app` from scripts (screenshots, CDP, verify).
Each launch leaves a `code_sign_clone` behind unless Chrome exits cleanly.
- Use Chrome for Testing instead (no clone), or use `agent-browser`.Limits and open questions
- I ran the
Browser.closetest four times, all on one machine and one Chrome version (156). Each run cleaned up its clone. - I cannot yet say whether the agent follows the new rule. A scan of my Claude Code session logs since the rule was added found no other script launching the real Chrome, and the folder holds only the clone of my running browser, but that is a few hours of data. If the folder fills up again, some script is still launching the real Chrome.