For two days Chrome kept dying mid-session and disappearing from my Applications folder. I would reinstall it, work for an hour, and it would be gone again. I filed it in my head as a bad auto-update and kept reinstalling.
It was not a bad auto-update. There were two pieces of malware on my laptop, and Chrome had nothing to do with either of them.
The first real clue was an absence. Chrome was missing from Applications, but there were no Chrome crash reports at all. None. An app that crashes leaves a report. An app whose executable is deleted out from under it just stops existing. That is a different failure, and it meant something was deleting files.
So I went looking for things that delete files, and found two launch agents in my own home directory that had no business being there. They were named com.apple.softwareupdate.agent and com.apple.softwareupdate.background. Apple's real software update daemon lives inside the System volume. These pointed at a binary in ~/Library/Caches, ad-hoc signed, no team identifier. Gatekeeper's verdict on it was one word: rejected.
The payload was a 145KB Python script. Its docstring claimed to be a ".NET Runtime Optimization Service" doing cache maintenance, which is a strange thing to find on a Mac. Further down, still in the docstring, was the line [LEGITIMATE DESCRIPTION REMOVED FOR OPSEC]. Somebody forgot to delete their own note to self.
What it actually did: watch the clipboard, and on every single change, grab the contents and take a screenshot. Encrypt both, POST them to a hardcoded IP every thirty seconds over plain HTTP. Its own log file told me it had been running for about twenty-three hours. Every password I had pasted. Every token. Everything that had been on my screen.
I killed it, pulled the launch agents, quarantined the files, and verified it was gone.
Three minutes later it was back.
That is the part worth writing about. There was a second implant I had not found, because it was never on disk. It ran as an inline node -e one-liner: fetch code from a command server, XOR-decode it, eval it. Nothing to grep for, nothing to quarantine, no file to delete. It sat in memory holding a live channel to whoever was on the other end.
The two of them kept each other alive. The Python stealer owned the disk persistence. The Node implant owned nothing but could re-download the stealer on command. Kill the stealer, and the Node process pulls a fresh copy from a file-sharing site and redeploys it. Kill the Node process, and the stealer brings it back. Either one alone regenerates the pair.
Sequential cleanup cannot work against that, no matter how carefully you do it. You have to kill both process trees in the same action and then clear persistence.
I did that. Thirteen minutes later, it beaconed again.
That was the moment the job changed. The second implant is not a process that sits there, it is a beacon: it wakes up, opens a connection, does whatever it is told, and exits. Point-in-time checks show a clean machine because most of the time the machine is clean. It spawns already orphaned, so by the time you see it, the thing that launched it is gone.
I went through every persistence mechanism macOS has. All four launchd directories. The full launchd session dump, in case a job was still loaded after its file was deleted. User and system shell configs. Cron. Login items. Git hooks. Python startup hooks. Editor extensions. Chrome native messaging hosts and extensions. Every global npm package under both Homebrew and nvm. Every package.json on the disk with an install hook that invokes node -e or curl or eval. All clean.
Which is its own kind of answer. When every standard mechanism is clean and the thing still comes back, the mechanism is not standard, and you are not going to grep your way to it.
Chrome, meanwhile, was genuinely innocent. Neither implant contained a single reference to Chrome, browser profiles, or cookies. The browser was being eaten by two generations of Google's own updater installed at the same time, fighting each other, with the "current" symlink pointing at the older one. Chrome updates by swapping the entire app bundle. The swap was failing after the old bundle came off. Two unrelated problems, and the noisy one was the harmless one.
On the AI part, since people keep asking. I did this with Claude Code driving the investigation, and the honest split is this: it was dramatically better than me at breadth and speed, and it was wrong about at least one thing.
What it was good at was the tedious middle. Reading half a megabyte of updater logs and pulling out the four lines that mattered. Walking a process tree to its parent before the parent exited. Diffing what a legitimate Apple daemon path looks like against what was actually there. When the malware came back during cleanup, it caught the reinfection live, captured the full parent chain and the open socket, and had the second implant's command line saved before I had finished reading the first alert. I would have gotten there eventually. Not in fifteen minutes.
What it got wrong: a teammate sent me a scanner script hosted under a GitHub org called OpenSourceMalware. Claude told me not to run it, on the reasonable-sounding grounds that the org name was alarming and blind curl-and-execute is how I probably got infected in the first place. Then it actually read the script and reversed itself. The thing is entirely read-only, just greps and a git reflog call, completely safe, and its signature list turned out to be the most useful lead of the whole day. The malware family it hunts uses a global['key']='victim-id' prologue followed by obfuscated JavaScript, which is exactly the shape of what was running on my machine. Good instinct, bad first call, corrected once it looked instead of guessed. Make it look.
So this post does not have the ending I wanted. I never found the entry point: the infection window contains nothing but WhatsApp telemetry and browser local storage, my shell history has no timestamps, and no dropper was recovered. I never found the trigger either. The machine is still compromised as I write this.
That is the honest state, and I think it is worth publishing in that state rather than waiting for a tidier one. A lot of security writing arrives pre-resolved, which quietly teaches you that if you are thorough enough you win. Sometimes you are thorough and you still lose, and the correct move is to stop spending hours on eradication and admit the machine is not salvageable. I am wiping it. Documents only, no application restore, no config restore, nothing out of ~/Library.
Rotating everything from a different device, starting with the wallet. I had MetaMask in two Chrome profiles, and a clipboard monitor plus a screenshot on every clipboard change is exactly the tool you would build to steal a seed phrase. I am treating mine as public.
If you are a developer, you are specifically who this family targets, usually through npm packages and fake recruiter take-home tests. Worth running npm config set ignore-scripts true today. Mine was set to false, like almost everyone's.
And if an app on your machine ever deletes itself, do not just reinstall it. Ask what has permission to delete files in that folder.