Opening a Folder Can Run Malware — A Safe Demo of Developer-Targeted Attacks Disguised as Job Recruiting via VS Code folderOpen and npm Lifecycle Scripts
When you clone a repository with git, all that really happens is that files get written to disk. But the moment you open that folder in an editor, or run npm install, code the repository planted can run automatically.
Lately, attacks that lure developers into running malware-laden repositories under the guise of recruiting or coding tests have been on the rise. The campaign that Palo Alto Networks’ Unit 42 named “Contagious Interview,” attributed to North Korea-linked actors, is the textbook case (see the Unit 42 report). Fake recruiters hand over a GitHub or Bitbucket repository as a “take-home assignment,” then coax the candidate into running npm install outside a container, which executes the malware (BeaverTail / InvisibleFerret), as documented in The Hacker News coverage.
The direct trigger for building this demo was the post below, in which developer Fabio Vedovelli reports that he almost ran malware on his own machine. He was sent a GitHub repository (a web3/poker “MVP”) with a request to clone and run it locally, dressed up as a technical test. It is exactly the pattern described above.
⚠️ Devs, parem tudo e leiam. Quase rodei malware na minha máquina agora mesmo e quero que vocês saibam exatamente como funciona o golpe.
— Fabio Vedovelli (@vedovelli74) May 26, 2026
Recebi um link de um repo no GitHub: um "MVP" de um projeto web3/poker, com pedido pra clonar e rodar localmente. Visual de teste técnico,… pic.twitter.com/XETTMelfCI
To understand this “runs the moment you open it” behavior by reproducing it safely on my own machine, I built a demo repository: vscode-folderopen-malware-demo. This post walks through its design decisions and the defenses the demo points to.
The repository is built safety-first. It contains no code that ships all of process.env and no runnable curl | bash-style payloads. What it actually sends over the network is limited to demo-scoped environment variables that start with CODENOTE_DEMO_, and the destination is pinned to the 127.0.0.1:47391 loopback address.
Why Opening a Repository Is Risky
Let me set one premise straight. git clone itself mostly just writes files. The dangerous parts are the two operations that usually follow:
- Opening the folder in an editor
- Running a package-manager command
Each of these has a legitimate mechanism for running code the moment you open or install. Attackers ride on those legitimate mechanisms, so they don’t even need a special vulnerability.
VS Code / Cursor folderOpen Tasks
VS Code has a Tasks feature that automatically runs a task when you open a folder. All it takes is this in .vscode/tasks.json:
{
"runOptions": {
"runOn": "folderOpen"
}
}A task with runOn: "folderOpen" becomes eligible to run the moment you open that folder. VS Code-derived editors such as Cursor share the same behavior.
npm Lifecycle Scripts
npm runs lifecycle scripts before and after install operations: preinstall, install, postinstall, prepare, and so on. Put these in the scripts field of package.json, and simply running npm install executes them.
In other words, the things you do almost reflexively right after git clone (opening it in the editor, running npm install) can be the code-execution trigger themselves.
What the Demo Repository Looks Like
The demo is a handful of small parts that work entirely within the loopback interface.
- collector (
apps/collector/server.js): a local server listening on127.0.0.1:47391that logs the variables it receives. It plays the role of an attacker’s collection server, but safely and only locally. - safe-env-client (
packages/safe-env-client/send-demo-env.js): a client that readsprocess.env, extracts only the variables starting withCODENOTE_DEMO_, and sends them to the collector. It is the safe version of malware’s “steal env vars and exfiltrate them” stage. - Two fixtures:
fixtures/npm-prepare-env-demo, which uses an npmpreparescript, andfixtures/vscode-folderopen-env-demo, which uses a folderOpen task. - Inert samples (
samples/*.sample.json): files that illustrate dangerous payloads such ascurl | bashin a form that cannot run. They serve as input for the scanner. - scan.js (
scripts/scan.js): a scanner that statically walks the repository and reports dangerous patterns. - walkthrough.js (
scripts/walkthrough.js): a script that guides you through the steps deterministically.
The flow from execution trigger to collector looks like this.
flowchart LR
subgraph trigger[Execution triggers]
A[VS Code<br/>folderOpen task]
B[npm prepare<br/>script]
end
A --> C[safe-env-client]
B --> C
C -->|CODENOTE_DEMO_ only| D[collector<br/>127.0.0.1:47391]
D --> E[Log with<br/>sensitive keys masked]
F[scan.js static scanner] -.detects.-> A
F -.detects.-> B
In real malware, the safe-env-client here would ship all of process.env to an external collection server, carrying off AWS access keys, GitHub tokens, and the contents of your .env along with everything else. The demo keeps that shape while narrowing both what is sent and where it goes to a safe range.
The Design Decisions That Keep It Safe
The thing I was most careful about was the line between “reproduce the attack faithfully” and “ruthlessly strip out anything that could cause real harm.” The concrete decisions:
- Only
CODENOTE_DEMO_variables are sent. The client filters by prefix before sending. No code anywhere in the repository ships all ofprocess.env. - The destination is pinned to loopback. The collection URL on the client side is a fixed constant that does not honor an environment-variable override. The collector also pins its bind address to
127.0.0.1, so passingCOLLECTOR_HOSTwill not widen it beyond loopback. - Sensitive-looking key names are masked. The collector replaces key names containing
SECRET,TOKEN,KEY,PASSWORD, orCREDENTIALwith***masked***before logging. - Dangerous examples cannot run.
curl | bashand PowerShell download-execute patterns are not present as runnable code; they exist only as strings inside.sample.jsonfiles. - Fixtures are kept out of the root workspaces. Because the npm fixtures are not part of the root
workspaces, runningnpm ciat the repository root does not run the demoprepare. The trigger only fires when you explicitly target the fixture. - No extra metadata is sent. The client payload drops
pid,cwd, andnodeVersion, keeping it to just what the demo needs.
A repository meant to teach dangerous behavior defeats its own purpose if it becomes dangerous itself. Most of the design effort went into keeping that safety boundary simple.
Threat Pattern A: VS Code folderOpen Tasks
The folderOpen task fixture (fixtures/vscode-folderopen-env-demo/.vscode/tasks.json) looks like this.
{
"version": "2.0.0",
"tasks": [
{
"label": "safe folderOpen env demo",
"type": "shell",
"command": "node ../../packages/safe-env-client/send-demo-env.js --demo vscode-folderopen-env-demo",
"presentation": {
"reveal": "always"
},
"runOptions": {
"runOn": "folderOpen"
},
"problemMatcher": []
}
]
}It differs from real malware in two ways. It only runs the local, safe client, and it sets presentation.reveal to "always" so the behavior is always visible in the terminal. Attackers set this to "never" to hide execution, which is exactly why the scanner below treats reveal: "never" as a suspicious signal.
What matters here is that VS Code itself has guardrails against this automatic execution. Open an untrusted folder and Workspace Trust prompts you; in Restricted Mode, tasks do not run. On top of that, the first time you open a folder containing a folderOpen task, VS Code asks whether to allow automatic tasks, and nothing runs until you explicitly approve via Tasks: Allow Automatic Tasks. This demo is both an attack path and a way to confirm those guardrails work.
When you want to observe the folderOpen behavior from a pristine state, launching VS Code with a throwaway profile is the safe way. Point --user-data-dir and --extensions-dir at temporary directories and every run starts clean, the Workspace Trust and automatic-task prompts appear each time, and your real settings stay untouched.
rm -rf /tmp/vscode-demo-profile /tmp/vscode-demo-ext
CODENOTE_DEMO_REGION=ap-northeast-1 \
CODENOTE_DEMO_GITHUB_TOKEN=fake-gh-token \
code --user-data-dir /tmp/vscode-demo-profile \
--extensions-dir /tmp/vscode-demo-ext \
fixtures/vscode-folderopen-env-demoEnvironment variables are inherited only by a freshly launched instance, so inject CODENOTE_DEMO_* on the code command line, or fully quit a running VS Code first.
Threat Pattern B: npm Lifecycle Scripts
The npm fixture (fixtures/npm-prepare-env-demo/package.json) is a minimal package with nothing but a prepare script.
{
"name": "npm-prepare-env-demo",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"prepare": "node ../../packages/safe-env-client/send-demo-env.js --demo npm-prepare-env-demo"
}
}Run npm install against this fixture with demo variables, and prepare runs.
CODENOTE_DEMO_REGION=ap-northeast-1 \
CODENOTE_DEMO_GITHUB_TOKEN=fake-gh-token \
npm install --package-lock=false --prefix fixtures/npm-prepare-env-demoThe safety measure here is the “keep fixtures out of the root workspaces” design mentioned earlier. Without it, simply running npm ci at the repository root would chain into the demo prepare. From an attacker’s point of view, that is precisely the “I only meant to install dependencies, but a planted lifecycle script ran too” situation. The demo cleanly separates the root install from fixture execution so the trigger only fires when intended.
Threat Pattern C: Environment Variable Collection
The safe-env-client reads process.env, then keeps only the variables starting with CODENOTE_DEMO_. The core of the extraction is just this.
function collectDemoEnv(env) {
return Object.fromEntries(
Object.entries(env)
.filter(([name]) => name.startsWith(DEMO_ENV_PREFIX))
.sort(([left], [right]) => left.localeCompare(right)),
);
}On the receiving end, the collector masks sensitive-looking key names before logging. When two variables are sent via prepare, the output looks like this.
[collector] received /collect from npm-prepare-env-demo
[collector] variableCount=2
CODENOTE_DEMO_GITHUB_TOKEN=***masked***
CODENOTE_DEMO_REGION=ap-northeast-1CODENOTE_DEMO_GITHUB_TOKEN is masked because its name contains TOKEN, and a variable without the prefix, such as SHOULD_NOT_SEND, is never even a candidate for sending. Real infostealers ship all of process.env here, scooping up AWS access keys, GitHub tokens, and the contents of .env in one go. Malware like Lumma Stealer, which I covered in an earlier post (The Vercel Breach Started with a Roblox Cheat Search), is exactly the kind of thing that automates this collection.
Inspecting an Unknown Repository with a Static Scanner
scan.js is a scanner for statically inspecting a repository before you open it. Because it only reads files rather than opening them in an editor, it surfaces dangerous patterns without ever tripping an execution trigger.
npm run scanThe scanner detects patterns like these:
- automatic tasks with
runOptions.runOn: "folderOpen" - execution hiding via
presentation.reveal: "never" - download-execute terms such as
curl,wget,bash,powershell,Invoke-WebRequest,iwr, andiex - npm lifecycle scripts
- the combination of
process.envwith a network call such ashttp.requestorfetch(
What makes it effective is how finely it casts the net. It inspects not just a task’s command but the contents of args too, joined together, and it matches terms case-insensitively, so mixed-case forms like CURL, PowerShell, and IEX are caught. It also reports code where process.env appears together with a network call as a separate signal that environment variables may be exfiltrated.
When you want CI to fail on high-severity findings, add --fail-on-high.
npm run scan -- --fail-on-highBy default the exit code stays 0 even when there are findings, so in CI it “reports but does not block.” The intent is to switch HIGH findings to blocking once the fixtures and sample expectations have been tuned.
Reproducing It Locally
The runtime is pinned to Node.js 26.2.0 via mise. The steps are roughly as follows.
# Provision the runtime
mise install
# Install dependencies (the fixture prepare does not run)
npm ci
# Start the collector in a separate terminal
npm run collector
# Fire the npm lifecycle trigger with demo variables
CODENOTE_DEMO_REGION=ap-northeast-1 \
CODENOTE_DEMO_GITHUB_TOKEN=fake-gh-token \
npm install --package-lock=false --prefix fixtures/npm-prepare-env-demo
# Inspect with the static scanner
npm run scanIf you want an interactive guide through the steps, use npm run walkthrough. It checks your Node.js version and whether the collector is reachable, then prints the command sequence above in order. Adding npm run walkthrough -- --run-safe-npm-demo actually runs only the safe npm fixture. Note that the walkthrough never opens VS Code for you. Opening a folder is, by design, something you do manually only after inspecting its contents.
The repository also bundles walkthrough skills for Claude Code and Codex, but the runnable behavior lives in ordinary scripts, and the skills only reference and guide you through them. The point is to keep important behavior auditable as ordinary project files rather than hiding it inside agent-specific instructions.
What to Check Before Opening an Unknown Repository
Before opening a repository of uncertain origin in an editor (a take-home assignment, a technical test, an unfamiliar piece of open source), it is safer to check the following in a browser or with static tooling.
- whether
.vscode/tasks.jsonhas arunOn: "folderOpen"task - the lifecycle scripts in
package.jsonsuch aspreinstall/postinstall/prepare - the contents of shell scripts and installer scripts
- patterns like
curl | bash,wget | sh, or PowerShell download-execute - code that reads
process.envand sends it over the network
git clone mostly just writes files, but opening a folder in an editor and running a package-manager command can execute code. For a repository of unknown origin, a single extra step, such as glancing at it in the browser or running it through static tooling before opening the folder, lowers the risk substantially.
If you have already opened or installed a suspicious repository, the safe move is to operate as if you have been compromised.
- Assume local credentials may have leaked, and rotate tokens, API keys, SSH keys, and cloud credentials.
- Stop processes originating from that repository, and disconnect the network if active exfiltration is suspected.
- Review shell history, editor tasks, package-manager logs, and process history.
- Rebuild development environments whose trust is now in doubt.
Wrapping Up
The takeaways from this demo come down to the following.
git cloneleans safe, but “open it in the editor” and “npm install” can mean code execution.- That execution rides on legitimate mechanisms: folderOpen tasks and npm lifecycle scripts.
- VS Code’s Workspace Trust and automatic-task approval are real guardrails, and a throwaway profile lets you confirm how they behave.
- A repository of unknown origin can have its risk cut substantially just by inspecting it statically before opening.
Reproduce the attack’s shape while narrowing what is sent and where it goes, and you can understand “what actually happens the moment you open it” with your own hands. Given that malware distribution disguised as recruiting assignments is now a real threat, making that “one extra step before opening” a habit is well worth it.
That’s all from the field, where I’ve been assembling a safe reproduction of the execution triggers that target developers.
References
- vscode-folderopen-malware-demo (the demo repository)
- VS Code Tasks documentation
- VS Code Workspace Trust
- npm scripts documentation
- Unit 42, “Hacking Employers and Seeking Employment: Two Job-Related Campaigns Bear Hallmarks of North Korean Threat Actors”
- The Hacker News, “North Korea-linked Supply Chain Attack Targets Developers with 35 Malicious npm Packages”
- Secure use reference for GitHub Actions
- mise