The Problem, In One Paragraph
VS Code lets a project define tasks in a file called .vscode/tasks.json. A task can say "run this the moment someone opens this folder." That file is committed to Git like any other file, so it travels with the repository into every clone, every branch, and every pull request.
Which means: if you open a folder, a repo you did not write can run a command as you with your SSH keys, your cloud credentials, and your npm tokens all sitting right there.
Nothing here is a bug. It is a normal feature working as documented. The three steps below take about five minutes.
Step 1 Turn Off Automatic Tasks
This is the single highest-value change, and it takes one minute.
Press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux), run Preferences: Open User Settings (JSON), and add these two lines inside the outer braces:
{
"task.allowAutomaticTasks": "off",
"security.workspace.trust.enabled": true,
// ... your existing settings
}Save and restart VS Code. That is the whole step.
The first line stops tasks from running when you open a folder. The second keeps Restricted Mode on, so an unfamiliar folder opens with tasks, debug configs, and risky extensions disabled until you explicitly trust it. They cover different things, so set both.
Cursor, Windsurf, and VSCodium are VS Code forks. They behave identically and keep separate settings files. Fixing VS Code alone leaves the fork wide open this is the most common gap I see.
# Where each editor keeps its settings
# macOS
~/Library/Application Support/Code/User/settings.json
~/Library/Application Support/Cursor/User/settings.json
# Linux
~/.config/Code/User/settings.json
# Windows
%APPDATA%\Code\User\settings.jsonOne habit matters as much as the setting: stop clicking "Yes, I trust the authors" out of reflex. If you always click yes, you have the security of having it turned off. Trust the repos you maintain. Leave everything you are reviewing in Restricted Mode.
Step 2 Check .vscode Before You Open a Repo
Cloning a repo runs nothing. Opening it is the dangerous step. So look first that ordering is the entire trick.
After cloning, before opening, read the file from your terminal:
git clone https://github.com/someone/repo.git
cd repo
# Does it want to run something on open?
cat .vscode/tasks.json 2>/dev/null | grep -A3 folderOpenIf that prints nothing, you are fine. If it prints a task, read what the command actually does before you open the folder.
Reviewing a pull request is the case that catches people out, because checking out someone's branch puts their files in your working tree. Check what the branch changed before you switch to it:
git fetch origin pull/482/head:review-482
# Did this PR touch anything that can execute?
git diff main..review-482 -- .vscode/ .devcontainer/ package.jsonA pull request that edits .vscode/ while claiming to fix a typo is not a puzzle to solve. That is the attack close it.
Do not open the folder in your editor to look at it. Read it from a terminal with cat or git show. Opening the folder is the trigger.
Step 3 Keep .vscode Out of Your Repos
Editor config is personal it is your window layout and your local paths, not part of the project. When it lives in the repo, it becomes a file that ships code to everyone who clones.
Add it to your global gitignore, once, for every repo you touch:
# Set up a global gitignore (once)
git config --global core.excludesfile ~/.gitignore_global
# Add editor config to it
cat >> ~/.gitignore_global <<'EOF'
.vscode/
.idea/
*.code-workspace
EOFNow you will never accidentally commit editor config to a shared repo again.
Be clear about what this does and does not do, though. A global gitignore stops you from adding these files. It does not protect you from a repo that already contains one gitignore only affects untracked files, and a committed .vscode/tasks.json is already tracked. That is what Step 1 and Step 2 are for. The three steps work together.
If your team genuinely needs shared editor settings, keep .vscode/settings.json and .vscode/extensions.json and leave tasks.json out. Then put the folder behind review so no one can add an auto-run task unnoticed:
# .github/CODEOWNERS
.vscode/ @your-org/platform-team
.devcontainer/ @your-org/platform-team
package.json @your-org/platform-teamWhat a Malicious Task Actually Looks Like
Worth seeing once, so you recognise it. This is the shape of a real one:
{
"label": "eslint-check",
"type": "shell",
"command": "node ./public/fonts/fa-solid-400.woff2",
"hide": true,
"presentation": { "reveal": "never", "close": true },
"runOptions": { "runOn": "folderOpen" }
}Every field is doing work. The label sounds like a linter. runOn: folderOpen is the trigger. reveal: never means the terminal never appears, and hide: true keeps it out of the task list. You see nothing at all.
The clever part is the payload. It is not a .js file it is a font, sitting in public/fonts/, with a Font Awesome name. Nobody reviews fonts. But the file is not a font at all, it is JavaScript, and the task hands it to Node.
That gives you a genuinely useful check, because it does not care how the code inside is written or obfuscated:
# A "font" that is actually a script
find . -name '*.woff2' -o -name '*.ttf' -o -name '*.otf' \
| while read f; do
file "$f" | grep -q 'ASCII text' && echo "NOT A FONT: $f"
doneA font that reports as text is wrong, always. Same idea works for any asset that should be binary and is not.
The Other Paths Worth Knowing
Automatic tasks are the widest hole, not the only one. You do not have to act on these today, but know they exist:
- ▸npm lifecycle scripts a postinstall in any dependency runs during npm install. This is the most exploited path in real supply chain incidents. npm install --ignore-scripts skips them.
- ▸Devcontainers .devcontainer/devcontainer.json can define initializeCommand, which runs on your host machine, not inside the container.
- ▸Git hooks a repo can ship hooks via .husky/ or point core.hooksPath at a directory inside itself.
- ▸Recommended extensions .vscode/extensions.json prompts you to install extensions, and a typosquatted name is a short path to code running with full editor access.
If You Think You Already Opened Something
Assume it ran as you. Check the usual persistence spots, then rotate credentials:
# Did something install itself to run later?
ls -la ~/Library/LaunchAgents/ # macOS
crontab -l # cron
tail -20 ~/.zshrc ~/.bashrc # appended lines
cat ~/.ssh/authorized_keys # added keysOne thing to be careful about: a task payload usually runs once and exits. Seeing no suspicious process in Activity Monitor does not mean nothing happened. Check the list above, which is what it leaves behind.
Then rotate, widest blast radius first: cloud credentials, tokens with push access to source control, npm and registry tokens, SSH keys, and anything in the .env files on that machine. If the machine can reach production, tell whoever owns that decision rather than quietly cleaning up and hoping.
The Five-Minute Checklist
- ▸Add task.allowAutomaticTasks: "off" and security.workspace.trust.enabled: true to User Settings (JSON)
- ▸Do the same in Cursor, Windsurf, or any other fork they keep separate settings files
- ▸Stop clicking "trust the authors" by reflex trust what you maintain, review the rest in Restricted Mode
- ▸cat .vscode/tasks.json before opening any repo you did not write
- ▸On pull requests, diff .vscode/ and package.json before checking the branch out
- ▸Add .vscode/ to your global gitignore so you never commit editor config
Wrapping Up
The uncomfortable part is that nothing here is broken. tasks.json works as documented, Git moves files as designed, and the result is still code running on your laptop before you have read anything.
What fails is an assumption: that reading code is passive. It stopped being passive when editors started running project config on open. A repository is not a document you inspect it is a program your tooling begins executing the moment you open it.
Two settings, one habit, one gitignore line. Against a threat that costs an attacker a single committed file, that is a very good trade.