Automated security checks for TypeScript monorepos
Por Seaworthy · 16 de mayo de 2026
Photo by Luca Bravo on Unsplash
Why monorepos make security harder before they make it easier
A monorepo is a great idea right up until you realize that "one repo" also means one sprawling attack surface, several competing node_modules trees, and a CI pipeline that either checks everything too slowly or skips things quietly.
TypeScript helps. Strong types mean fewer injection vectors, fewer runtime surprises, and better tooling. But TypeScript is not a security policy. It doesn't audit your dependencies. It doesn't check whether you committed a .env file. It doesn't notice that your shared config package is exposing a database URL to a public-facing web app.
So you need automated checks on top of it. Here's how to build them into a monorepo without losing your mind.
The workspace dependency problem
In a pnpm or npm workspace monorepo, each package can declare its own dependencies, but they all resolve through the same lockfile. That sounds tidy. The problem is that npm audit and pnpm audit report vulnerabilities at the root level, and it's not always obvious which workspace package actually consumes the vulnerable module.
Run this to get a clearer picture:
# pnpm workspaces: audit each package independently
pnpm --filter './packages/*' exec pnpm audit --audit-level=high
That --filter flag tells pnpm to walk every package under packages/ and run the audit in context. You get per-package output instead of one giant flat list. If you're on npm workspaces, the equivalent is:
npm audit --workspaces --include-workspace-root
Neither of these replaces reading the output carefully. A critical vulnerability in a dev-only build tool is a different risk than one in your API server. Automate the command; don't automate the judgment.
Shared packages are shared risk
The whole point of a monorepo is sharing code: a utils package, a config package, a types package. But shared packages create a sneaky security pattern where a secret or a vulnerable dependency gets pulled into every consumer without anyone noticing.
Two things to check automatically:
First, scan shared packages for hardcoded secrets. Something like gitleaks or trufflehog run on the packages/ directory catches API keys and tokens before they propagate to every downstream consumer.
Second, track which packages import which. You almost certainly don't want your public-facing web package importing from server or infra. A tool like dependency-cruiser can enforce this:
{
"forbidden": [
{
"name": "no-web-to-server",
"from": { "path": "packages/web" },
"to": { "path": "packages/server" }
}
]
}
Add depcruise --validate .dependency-cruiser.json packages/ to your CI and it becomes a hard gate, not a polite suggestion.
TypeScript config as an attack surface
tsconfig.json files are boring until they aren't. A few settings worth checking automatically:
"noImplicitAny": false — TypeScript's best security property is that it forces you to think about what types are flowing through your code. Turning this off silently opens the door to untyped third-party data reaching your business logic.
"skipLibCheck": true — Fine for development speed; risky in production builds. It skips type-checking your node_modules, which means a malicious or corrupted type definition won't be caught at compile time.
"paths" aliases that resolve to absolute filesystem paths — I've seen this go wrong in creative ways when a path alias in a shared tsconfig.base.json points somewhere unexpected. Audit your path mappings.
You can write a small script that walks packages/*/tsconfig.json and flags these patterns, or fold the checks into a custom lint rule.
CI setup that actually runs everything
The failure mode in monorepos is selective CI: the pipeline runs security checks only on changed packages to save time, and the shared config package never changes, so its vulnerabilities never get checked.
A reasonable middle ground is to run full security checks on a schedule, not just on pull requests:
# .github/workflows/security.yml
on:
schedule:
- cron: '0 6 * * 1' # every Monday at 06:00 UTC
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm --filter './packages/*' exec pnpm audit --audit-level=high
- run: pnpm exec gitleaks detect --source . --no-git
- run: pnpm exec depcruise --validate .dependency-cruiser.json packages/
The scheduled run catches vulnerabilities disclosed after your last commit. The push trigger catches regressions you introduce. Neither of these is optional if you care about the server package staying clean.
What static analysis misses
Automated checks are fast and consistent. They are also shallow. They will not tell you that your authentication middleware has a logic flaw, that your rate limiting applies to the wrong route, or that your monorepo's Docker build stage copies more files than it should.
For that you need a tool that understands the full shape of what you're shipping: the ops basics, the configuration patterns, the data exposure risks. Static analysis tools that only look at dependencies are only reading half the code.
Seaworthy is a static analysis tool that scans a repository across six categories, including security, configuration, and data exposure, and is available via npx seaworthycode or as a VS Code extension.
Este artículo fue generado por IA y resume fuentes públicamente disponibles.
Seaworthy analiza tu repositorio en busca de los problemas que se tratan en artículos como este.
Brechas de seguridad, secretos expuestos y configuraciones erróneas detectados antes de desplegar. Gratis, sin cuenta necesaria.