Safeguard
Open Source

@angular-eslint/builder: What It Is and How to Use It Safely

The @angular-eslint/builder package wires ESLint into the Angular CLI so you can run ng lint. Here's how it fits your build and where the supply-chain risk actually lives.

Karan Patel
Platform Engineer
7 min read

The @angular-eslint/builder package is an Angular CLI builder that lets you run ESLint through ng lint, and while it never ships in your production bundle, it still deserves the same supply-chain scrutiny as any other dependency. It is one piece of the larger angular-eslint project, the toolchain that replaced the deprecated TSLint story for Angular workspaces.

If you have ever run ng lint and wondered what actually executes ESLint under the hood, this is it. The builder registers itself as an Angular architect target so the CLI knows how to invoke ESLint with your project's configuration, file globs, and reporters.

What the builder actually does

The Angular CLI is built around "builders" (also called architect targets). Each target in angular.json names a builder and passes it options. @angular-eslint/builder provides the lint target implementation. When you run ng lint my-app, the CLI looks up the lint architect entry, sees it points at this builder, and hands off to it.

A typical angular.json entry looks like this:

"lint": {
  "builder": "@angular-eslint/builder:lint",
  "options": {
    "lintFilePatterns": [
      "src/**/*.ts",
      "src/**/*.html"
    ]
  }
}

The builder resolves those globs, loads your eslint.config.js (flat config) or .eslintrc.json, runs ESLint programmatically, and translates the results into the format the Angular CLI expects. It is a thin adapter. The real work is done by ESLint itself, plus the Angular-specific plugins @angular-eslint/eslint-plugin and @angular-eslint/eslint-plugin-template.

Because the angular-eslint/builder sits in devDependencies, it runs only on developer machines and CI. It is never bundled into the JavaScript you ship to browsers. That fact shapes the entire risk conversation.

Where the real security risk lives

Dev dependencies feel safe because they do not reach production, but that intuition is wrong in two important ways.

First, a linter builder runs with full access to your source tree and your CI environment. During a lint run it can read every file, and it executes arbitrary Node code from its own dependency tree. If any transitive package in that tree were compromised, the malicious code would execute in an environment that frequently holds npm tokens, cloud credentials, and signing keys. CI is a high-value target precisely because build tooling runs there unsandboxed.

Second, the install step itself is a risk surface. npm install can run lifecycle scripts (preinstall, postinstall) for any package in the tree. This is the vector behind most real-world npm supply-chain incidents: a package or one of its dependencies publishes a malicious version that exfiltrates environment variables during install.

So the threat model for @angular-eslint/builder is not "does the builder have a bug that corrupts my app." It is "what does the builder's dependency tree pull in, and what can run during install and lint."

Pinning and verifying the dependency

Start by treating the lint toolchain as version-pinned infrastructure, not a floating range. Use a lockfile and commit it. package-lock.json or pnpm-lock.yaml records exact resolved versions and integrity hashes, so a fresh npm ci reproduces the same tree every time.

Check what you actually resolved:

npm ls @angular-eslint/builder
npm ls eslint

Keep the angular-eslint packages aligned with your Angular major version. The project publishes majors that track Angular releases, so a mismatch between your Angular CLI and the builder is the most common source of "it lints locally but breaks in CI" reports. When you upgrade Angular, upgrade the whole angular-eslint family together rather than one package at a time.

Run an audit on every install and wire it into CI:

npm audit --omit=dev=false

An audit alone only catches advisories that already exist in a database. It will not catch a brand-new malicious publish. For that, disable lifecycle scripts for packages you do not trust by default. npm supports --ignore-scripts, and pnpm lets you allowlist which packages may run install scripts. Turning scripts off globally and allowlisting the few that genuinely need them (native builds, mostly) closes the most common exfiltration path.

Locking down the CI environment

Because the builder runs in CI, harden that environment rather than trusting the package.

  • Give the lint job the minimum credentials it needs. A pure ng lint step needs read access to source and nothing else — no npm publish token, no cloud secrets.
  • Run installs with npm ci, not npm install, so the lockfile is authoritative and no ranges silently resolve to a newer version.
  • Cache by lockfile hash so a changed dependency tree forces a clean resolve.

If you want a signal earlier than CI, an SCA tool such as Safeguard can flag known-vulnerable versions anywhere in the transitive tree — including dev-only packages like this builder — before they land on main. That catches the "someone bumped a range and pulled in a flagged transitive dep" case that audits often miss during code review. Software composition analysis is the discipline that covers exactly this.

Migrating from the legacy builder

Older Angular workspaces used TSLint via @angular-devkit/build-angular:tslint. TSLint has been deprecated for years and receives no fixes. If your angular.json still references a TSLint target, that is a genuine maintenance-status problem: you are linting with a tool that no longer gets security or correctness updates.

The migration path is to run the angular-eslint schematic, which swaps the target over to @angular-eslint/builder:lint, generates a starting ESLint config, and converts what it can from your old TSLint rules:

ng add @angular-eslint/schematics

After that, review the generated config. The automatic conversion is best-effort, and a handful of TSLint rules have no direct ESLint equivalent, so you will want to fill gaps by hand.

A sensible baseline config

Once the builder is in place, the config that matters for security is ESLint's own. Keep the type-aware rules on for TypeScript, because rules that read type information catch a class of bugs that syntactic rules cannot. A minimal flat config:

// eslint.config.js
const angular = require("angular-eslint");

module.exports = [
  ...angular.configs.tsRecommended,
  {
    files: ["**/*.ts"],
    languageOptions: {
      parserOptions: { project: ["./tsconfig.json"] }
    }
  },
  ...angular.configs.templateRecommended
];

Linting is not a substitute for real security testing, but a well-configured ruleset catches unsafe patterns — like binding untrusted values into innerHTML templates — before they become findings. Pair it with proper DAST coverage for the running app, since the linter can only see source, not runtime behavior.

FAQ

Is @angular-eslint/builder safe to use?

Yes. It is the maintained, officially recommended way to run ESLint in Angular workspaces. The security work is not about avoiding the builder — it is about pinning versions with a lockfile, auditing the transitive tree, and hardening the CI environment where it runs.

Does @angular-eslint/builder end up in my production bundle?

No. It is a dev dependency used only at build and lint time. It never ships to the browser. That does not make it risk-free, because it executes with full access to your source and CI credentials.

How do I know which version to install?

Match the angular-eslint major version to your Angular major version, and upgrade the whole family together. Check the versions actually resolved with npm ls @angular-eslint/builder.

What replaced TSLint for Angular?

The angular-eslint project, of which @angular-eslint/builder is the CLI integration piece. Run ng add @angular-eslint/schematics to migrate an existing TSLint workspace.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.