cat posts/2026-07-06-the-backdoor-in-the-job-offer.md

The backdoor in the job offer

· 6 min read · security incidents agents npm social-engineering

The alert came in at 4:12 on a Thursday afternoon, from the endpoint agent on one of our engineers’ Macs: a node process, started from a folder in her Downloads, had made an outbound connection to a host nobody had seen before, then started a second process that tried to read the browser’s cookie store. Falcon killed the second process. It didn’t kill the first one, which had already done its job, which was to ask a server what to do next.

A couple of weeks earlier Roman Imankulov had written up almost exactly this, and it did the rounds on Hacker News. A recruiter for a small crypto startup messaged him on LinkedIn, chatted for a couple of days, and then sent a public GitHub repo with a request to “check out the deprecated Node modules issue.” He was suspicious, so he cloned it onto a throwaway server, didn’t install anything, and pointed a coding agent at it with only file-reading tools turned on. The agent found the problem in seconds: a test file that wasn’t a test, pulled in from the main app, and a prepare script in package.json so that a plain npm install would fetch and run whatever a remote server sent back. The commits were attributed to a real developer who’d never heard of the project, and the recruiter’s profile belonged to a real arts journalist, who became surprisingly fluent in Node versions once he pretended the install wasn’t working.

I posted the link in our engineering channel. Several people reacted with the eyes emoji. One of them, it turned out, had a nearly identical conversation going in her LinkedIn inbox at the time, though she didn’t connect the two until afterwards.

What she did

Hers was a “fintech” with a very plausible careers page, offering paid consulting, a few hours a week. Plenty of engineers get these and some take them, and we don’t have a rule against it, as long as it’s not on company time or company hardware. She knew that. She also had ten minutes before a meeting, and the repo was small, and she figured she’d just have a look.

She cloned it into Downloads on her work laptop, opened Claude Code in the folder, and asked it to figure out why the dependencies were complaining. The agent read package.json, read a few files, and then did what any of us would have done, which is ask to run npm install to see the actual warnings. She pressed y.

I want to be careful here, because she’s told this story to the whole team herself, without being asked, and she’s been harder on herself than I would be. The agent wasn’t fooled by anything clever. It didn’t read the test file, because there was no reason to; nothing in the task pointed at tests. It asked to run the one command that triggered the payload, because that’s the obvious first step for the task it was given, and the task was chosen by the attacker for exactly that reason. Imankulov’s agent found the backdoor because he’d asked it to look for something suspicious and taken away its ability to run anything. Hers was asked to fix a dependency problem, with the normal permissions an engineer uses all day.

The four hours

The endpoint alert went to our IT provider and to me. She’d noticed something too, because the install printed nothing unusual and then Falcon popped a notification, and she messaged me before I’d finished reading the alert, which helped a lot.

We isolated the laptop from the network through the endpoint console at 4:20. Then we went through what that machine could reach, and the list was longer than I’d have guessed.

Her SSO session was the big one. Our cloud access is short-lived credentials through SSO, which we moved to years ago so nobody has long-lived AWS keys on a laptop, and her session was still valid. The endpoint agent had stopped the cookie read, as far as we could tell from its logs, but “as far as we could tell” isn’t something you stake your production account on. We killed all her sessions at the identity provider, which logs her out of everything at once, reset her password, and had her re-register both of her security keys, which engineering moved to earlier this year. Then GitHub: her personal access token for the CLI, revoked, and her SSH key removed from her account. Then npm, where she had a token she’d used once for something years ago and forgotten. Then the API key for her Claude Code session, which lives in the macOS keychain and is the kind of thing I’d never have thought to put on the list two years ago. Then the database client, which had saved connection details for the staging database, read-only, with a password we rotated anyway.

While that was going on, someone else pulled the audit logs for every one of those systems for the previous hour. There was nothing: no logins from anywhere unexpected, no tokens used from outside her laptop, no new SSH keys added, no new OAuth apps. The connection to the remote server had lasted about forty seconds before Falcon’s second-process kill, and whatever it fetched, it didn’t get far enough to show up anywhere we could see. The IT provider wiped and reimaged the laptop that evening, and she was back working the next morning, on a machine with nothing on it she’d need to remember.

We also reported the repo to GitHub and the recruiter’s profile to LinkedIn. The repo was gone two days later. The profile’s still up as I write this.

What’s different now

The guidance part was easy. Take-home coding tests and “can you look at our repo” requests from anyone outside the company happen in a throwaway VM or a cloud box with nothing in it, never on a work laptop, and preferably never with an install step at all until you’ve read what the install will run. Our sandbox, the container we use for unattended agent runs, was already the right shape for this: no credentials, a registry-only network, gone when you’re done. We’d built it to protect us from our own agents, and it turns out to be just as good for running a stranger’s code. I wrote a one-page note on using it for exactly that, and the npm install that runs in there sees nothing worth stealing.

We also turned on npm’s ignore-scripts setting as a default on engineering laptops, so that installing doesn’t run package scripts unless you ask it to. It breaks a handful of packages with native builds, which now need an explicit rebuild, and two people have already grumbled about it. I think it’s worth it. It wouldn’t have helped in the September npm compromise I wrote about last year, where the payload was in the package code itself, but it would have stopped this one cold.

The last change is about the approval prompt. Every engineer on the team presses y on a few hundred agent requests a week, and she did read this one. It said npm install, which on any of the other repos she’d been in that day would have been fine. The prompt shows you the command, not whose code it’s about to run, so we stopped relying on it for this case. An agent pointed at a repo from outside the company now gets read-only tools on a laptop, the way Imankulov ran his, and anything that needs to execute goes to the sandbox. In Claude Code that’s a command-line flag that turns off the shell and edit tools, and it’s the first line of the one-page note.