Skip to content
Back to skills

Workspace Audit

ASecurity

Audit a pnpm 10+ workspace for configuration and monorepo problems: dependency placement, workspace protocol usage, hoisting, catalog configuration, build order and build hooks, dependency rules, and .npmrc settings that are now defaults. Reads and reports only.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 23, 2026
developmenttypescriptrustgoshellbashreactnodegitsecurity

Works with

  • claude code
  • cli

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 3 files and shows the line behind each finding

Scanned October 1, 2026

npx -y skills add edloidas/skills --skill workspace-audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Workspace Audit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Workspace Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/edloidas-workspace-audit/badge)](https://www.skillsdirectory.com/skills/edloidas-workspace-audit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: workspace-audit
description: >
  Audit a pnpm 10+ workspace for configuration and monorepo problems: dependency placement,
  workspace protocol usage, hoisting, catalog configuration, build order and build hooks,
  dependency rules, and .npmrc settings that are now defaults. Reads and reports only.
when_to_use: >
  When setting up or cleaning up a pnpm monorepo, diagnosing why a dependency resolves oddly
  across packages, or reviewing `pnpm-workspace.yaml`. Also on "audit the workspace", "check
  the monorepo setup", "pnpm catalog", "hoisting", or when wiring build order through
  Turborepo or Nx.
license: MIT
compatibility: Claude Code, Codex, OpenCode, Pi
allowed-tools: Bash(pnpm dedupe --check) Bash(pnpm -r ls:*) Bash(turbo run build --dry-run:*) Bash(npx syncpack list-mismatches:*) Bash(npx madge --circular:*) Read Glob Grep
---

# Workspace Audit (pnpm 10+)

## Purpose

Analyze pnpm workspace configuration — dependency placement, workspace protocol and catalog
usage, build hooks, dependency rules, and cargo-culted `.npmrc` settings.

**This skill reads and reports; it never edits the workspace.** Every command below is a read or a
dry run (`pnpm dedupe --check`, `turbo run build --dry-run`, `syncpack list-mismatches`), so no
config file, lockfile, or `node_modules` tree changes. The one command that would write is
`pnpm -r run build` in Step 6 — it is there for a user who asks for it, not for the audit.

Scoped to pnpm 10+ deliberately: catalogs, `allowBuilds`, `minimumReleaseAge` and the rest are
pnpm's own fields, and none of it transfers to npm, yarn or bun. On a repo whose lockfile is
`package-lock.json`, `yarn.lock` or `bun.lock`, say so and stop rather than translating the advice.

## Workflow

Done means: the Step 11 report is printed, with every step either reported or listed under
**Not checked** — or Step 1 printed `Not a pnpm workspace` and stopped.

Steps 2–10 each close with one line carrying their counts, such as `Step 5: 6 shared deps, 1
hardcoded, catalogMode unset.` A step with nothing to check says why in that line.

### Step 1: Identify Workspace Type

Glob the repo root for `pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`, `bun.lock`, `nx.json`,
and `turbo.json`. If no `pnpm-lock.yaml` exists, print `Not a pnpm workspace — <lockfile> found.`
and stop.

Read `pnpm-workspace.yaml` and the root `package.json`. The `packageManager` field gives the exact
pnpm version; `engines` may pin it too. Audit advice below assumes pnpm 10+; call out version-gated
settings when the project is on an older minor.

Start a **not-checked list** here and carry it to Step 11. Every step below that cannot run goes
in it with its reason: an absent `.npmrc` or `turbo.json`, a `pnpm` binary too old for a
version-gated field, an `npx` tool (`syncpack`, `madge`) that is not installed or refused to
fetch. A report that names only the findings reads as a full audit of everything else.

Close the step with one line: `Workspace: pnpm 10.28.1, 7 packages (3 apps, 4 libs), turbo.json present, no nx.json.`

### Step 2: Analyze Workspace Structure

```yaml
# Good: Explicit patterns
packages:
  - 'packages/*'
  - 'apps/*'
  - 'tools/*'

# Avoid: Too broad
packages:
  - '**'
```

**Config-only workspace:** A `pnpm-workspace.yaml` without a `packages:` field is valid for single-package projects that still want catalog, overrides, or build hook config.

### Step 3: Check Workspace Protocol Usage

**Good:**
```json
{
  "dependencies": {
    "@myorg/shared": "workspace:*",
    "@myorg/utils": "workspace:^"
  }
}
```

**Audit:** flag a hardcoded version (`"^1.0.0"`) on an *internal* package — it should be
`workspace:*`. Third-party deps are Step 5's rule, not this one.

Grep every `package.json` under the `packages:` globs for the workspace's own scope (`"@myorg/`),
and flag each hit whose range does not start with `workspace:`.

### Step 4: Check Dependency Hoisting

Pnpm uses isolated `node_modules` by default — no hoisting. Check `.npmrc` for overrides:

```ini
hoist=true            # enables hoisting to .pnpm/node_modules
shamefully-hoist=true # makes node_modules flat like npm — last resort
```

**Audit:** `shamefully-hoist=true` is a red flag. It bypasses pnpm's isolation model. Should only be present if a specific tool requires it, with a comment explaining why.

```bash
pnpm dedupe --check
```

### Step 5: Check Catalog Configuration

#### Catalog definition
```yaml
# pnpm-workspace.yaml
catalog:
  react: ^19.0.0
  react-dom: ^19.0.0
  typescript: ^5.0.0
  vite: ^6.0.0
  vitest: ^3.0.0

  # npm: prefix aliases a name to a different package implementation
  # vite: npm:@org/custom-vite-fork@^1.0.0
```

#### Usage in packages
```json
{
  "dependencies": { "react": "catalog:", "react-dom": "catalog:" },
  "devDependencies": { "vite": "catalog:", "vitest": "catalog:", "typescript": "catalog:" }
}
```

#### Audit checks

**The rule: a third-party dep used by two or more packages is declared once in the catalog and
referenced as `catalog:` everywhere.** A hardcoded version in an individual package is the finding.

- `catalogMode: force` (10.12.1+) makes pnpm enforce that rule at install time. Flag it as unset
  wherever a shared dep exists.
- Avoid `@latest` in catalog entries — defeats reproducibility and conflicts with `minimumReleaseAge`
- `npm:` aliases must be intentional and version-pinned (not `@latest`)
- `cleanupUnusedCatalogs: true` (10.15+) — automatically removes stale catalog entries on install; flag if catalog has grown large and this is not set

### Step 6: Check Build Order

Verify cross-package dependencies are declared:

```json
{
  "name": "@myorg/app",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}
```

```bash
turbo run build --dry-run   # if Turbo is used — prints the order, builds nothing
pnpm -r ls --depth -1       # topological package list
```

`pnpm -r run build` proves the order end to end but writes build output, so the audit does not run
it. Offer it as a follow-up when the declared graph looks wrong.

### Step 7: Check Build Hook Configuration

Build hook config belongs in `pnpm-workspace.yaml`, **not** `package.json`.

#### pnpm 10.0–10.25

```yaml
# Blacklist: skip post-install scripts for these
ignoredBuiltDependencies:
  - unrs-resolver
  - sharp

# Whitelist: only these packages may run post-install scripts
onlyBuiltDependencies:
  - esbuild
```

Don't use both together — pick the model that fits the project's security posture.

#### pnpm 10.26+ — `allowBuilds`

Replaces `ignoredBuiltDependencies` and `onlyBuiltDependencies` with a single explicit map:

```yaml
allowBuilds:
  esbuild: true
  unrs-resolver: false
  sharp: false
```

#### `strictDepBuilds` (10.3+)

Fails the install if any dependency tries to run a build script that isn't covered by the allow/ignore config:

```yaml
strictDepBuilds: true
```

**Audit:** If neither `onlyBuiltDependencies` / `allowBuilds` nor `strictDepBuilds` is set, build scripts run unchecked — flag as a supply-chain risk.

### Step 8: Check Dependency Rules

All of these belong in `pnpm-workspace.yaml`.

#### minimumReleaseAge (10.16+) / minimumReleaseAgeExclude (10.17+)

Prevents installing packages published less than N minutes ago:

```yaml
minimumReleaseAge: 1440  # 24 hours

minimumReleaseAgeExclude:
  - '@typescript/native-preview'  # bleeding-edge, exempt by design
```

Audit: missing entirely is a risk signal. `1440` (24h) is a reasonable default.

#### trustPolicy (10.21+)

Enforces publisher trust levels — complements `minimumReleaseAge` with a signature/provenance check:

```yaml
trustPolicy: audit       # audit | warn | off

trustPolicyExclude:      # (10.22+) exempt specific packages
  - '@myorg/internal'

trustPolicyIgnoreAfter: 525600  # (10.27+) ignore trust for packages older than 1 year
```

Audit: flag if `trustPolicy` is absent and the project has `minimumReleaseAge` set — both are supply-chain controls that complement each other.

#### blockExoticSubdeps (10.26+)

Restricts git, file, and URL dependencies to direct dependencies only — prevents transitive exotic sources:

```yaml
blockExoticSubdeps: true
```

Audit: flag if git or file deps appear in the dependency tree and this is not enabled.

#### overrides

Pin or replace transitive dependency versions, including `catalog:` references:

```yaml
overrides:
  vite: 'catalog:'    # force transitive consumers to use the catalog version
  vitest: 'catalog:'
  lodash: '^4.17.21' # pin vulnerable transitive dep
```

Audit: check for outdated pinned versions, or missing overrides where catalog versions are inconsistent across the dep tree.

#### peerDependencyRules

Suppress spurious peer dep warnings — common with custom toolchain forks:

```yaml
peerDependencyRules:
  allowAny:
    - vite
    - vitest
  allowedVersions:
    vite: '*'
    vitest: '*'
```

Audit: flag `allowAny: ['*']` — that's too broad. Specific package names are fine.

### Step 9: Check .npmrc

Most `.npmrc` settings commonly copy-pasted into projects are redundant defaults or belong elsewhere:

| Setting | Issue |
|---|---|
| `auto-install-peers=true` | Default in pnpm 9+ — redundant |
| `prefer-frozen-lockfile=true` | Default in pnpm 10 — redundant; use `--frozen-lockfile` CLI flag in CI for hard-fail behavior |
| `prefer-workspace-packages=true` | Superseded by `workspace:` protocol |
| `strict-peer-dependencies=true` | Not default; use only if you want hard failures on peer mismatches — evaluate per-project |

**Audit: flag `.npmrc` entries that are no-ops or have better homes.**

Valid reasons to use `.npmrc`:
```ini
# Private registry for scoped packages
@myorg:registry=https://npm.myorg.com/

# Windows cross-platform script compatibility
shell-emulator=true
```

### Step 10: Check for Common Issues

Both tools below are fetched by `npx` and may be unavailable offline or blocked by a registry
policy. Run each, and put any that does not run on the not-checked list with its reason —
`syncpack` and `madge` cover findings no other step produces, so a silent skip leaves a gap the
report would otherwise appear to have covered.

#### Inconsistent dep versions (not in catalog)
```bash
npx syncpack list-mismatches
```

#### Circular dependencies
```bash
npx madge --circular packages/*/src
```

#### Stale catalog entries
For each key under `catalog:` and each named catalog, grep the workspace's `package.json` files for
`"<key>": "catalog:`. A key nothing references is stale. Recommend `cleanupUnusedCatalogs: true`,
which prunes such keys on the user's next install — the audit does not run that install.

### Step 11: Generate Report

One section per step group, `[x]` for what holds and `[ ]` for each finding, then a numbered
**Recommendations** list ordered worst-first, then **Not checked** — one line per entry on the
list started in Step 1, with its reason.

A filled report:

```markdown
## Workspace Audit Report

### Structure
- pnpm 10.28.1 (`packageManager`), 7 packages (3 apps, 4 libs), turbo.json present

### Workspace Protocol
- [x] `workspace:*` on 11 of 13 internal refs
- [ ] `apps/web/package.json` and `apps/docs/package.json` pin `@acme/ui: "^2.1.0"` — a published
      version, so a local edit to `packages/ui` is not picked up. Use `workspace:*`

### Catalog
- [x] `react`, `react-dom`, `typescript`, `vitest` declared once and referenced as `catalog:`
- [ ] `vite` is `^7.0.0` in `apps/web` and `^7.1.2` in `apps/docs` — shared, so it belongs in the
      catalog
- [ ] `catalogMode` unset — nothing stops the next package from hardcoding a catalogued dep

### Build Hooks
- [x] `onlyBuiltDependencies: [esbuild, sharp]` in `pnpm-workspace.yaml`
- [ ] pnpm is 10.28, so `onlyBuiltDependencies` is the legacy split key. Migrate to `allowBuilds`
      (`pnpx codemod run pnpm-v10-to-v11`) before the v11 bump removes it
- [ ] `strictDepBuilds` unset — a new dependency requesting a build script is skipped silently

### Dependency Rules
- [ ] `minimumReleaseAge` unset — a package published 40 seconds ago installs. Set `1440`
- [ ] `trustPolicy` unset
- [x] `overrides` pins `vite` and `vitest` to `catalog:`
- [ ] `blockExoticSubdeps` unset while `packages/legacy` pulls a `git:` transitive dep

### Configuration
- [ ] .npmrc sets `prefer-frozen-lockfile=true` (pnpm 10 default) and
      `auto-install-peers=true` (pnpm 9+ default) — both no-ops, delete them
- [x] `@acme:registry=https://npm.acme.dev/` is a legitimate .npmrc entry

### Recommendations
1. Set `minimumReleaseAge: 1440` and `trustPolicy: audit` in `pnpm-workspace.yaml`
2. Enable `strictDepBuilds: true` and migrate to `allowBuilds`
3. Move `vite` into the catalog and set `catalogMode: force`
4. Replace the two hardcoded `@acme/ui` versions with `workspace:*`
5. Enable `blockExoticSubdeps: true` once the `git:` transitive dep in `packages/legacy` is gone
6. Delete the two redundant .npmrc lines

### Not checked
- Circular dependencies — `npx madge` could not fetch; the registry is behind a proxy
- Build order — no `nx.json`, and `turbo run build --dry-run` needs an install this audit
  does not perform
```

Then stop. Report the findings; do not edit `pnpm-workspace.yaml`, a `package.json`, or .npmrc,
and do not run `pnpm install` or `pnpm dedupe` without `--check`.

See `references/workspace-template.md` for an optimized pnpm-workspace.yaml template.

Files in this skill

  • SKILL.md12.7 KB
  • agents/openai.yaml161 B
  • references/workspace-template.md3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.