A terminal showing a successful secret scan with a watchful gray-blue cat nearby.

Secretlint: Catching Secrets Before They Ship

The Gray Cat
The Gray Cat
0 views

Every developer has, at least once, felt that cold drop in the stomach after git push — the realization that an .env line, an AWS key, or a private RSA block just rode along into a commit. Once a credential lands in git history (especially on a public repo), the damage is done: bots scrape public commits within seconds, deletion from history is painful, and the only safe move is to rotate the key. Secretlint is built to stop that moment from ever happening. It scans your files locally, before commit, and flags anything that looks like a leaked credential.

What makes it interesting is its shape. Authored by azu — the same mind behind textlint — Secretlint borrows that pluggable architecture wholesale. The secretlint core ships with zero detection rules of its own. All the intelligence lives in installable npm plugins that you opt into. If you already run ESLint and Prettier through husky and lint-staged, Secretlint drops into that exact same pipeline and is configured the same way: an rc file plus some plugins.

Why Another Secret Scanner

There are several good tools in this space, and Secretlint carves out a deliberate niche rather than trying to beat everyone at everything.

  • Opt-in, not opt-out. Tools like Yelp's detect-secrets turn everything on and ask you to suppress the noise with a baseline file. Secretlint flips that: you enable only the rules you need, which keeps false positives genuinely low.
  • Runs locally, pre-commit. GitHub's own secret scanning runs server-side after you push. Secretlint catches the secret while it's still on your machine, where the fix is free.
  • Rules double as documentation. Each rule emits a message explaining why something was flagged, with a link to its docs — so a flagged line teaches rather than just blocks.
  • Secrets are masked by default. The output never prints the real credential, so dumping results into CI logs (or pasting them into an AI assistant) doesn't re-leak the very thing you're trying to protect.
  • Custom rules are first-class npm packages. Need to detect your company's internal token format? Write a rule, publish it, share it. No forking, no wrestling with a monolithic regex config.

For JavaScript and TypeScript shops, that last point compounds nicely: Secretlint lives where your other dev tooling already lives.

Getting It Installed

The Node.js path is the most common. You install the core plus at least one rule package — almost everyone starts with the recommended preset.

npm install secretlint @secretlint/secretlint-rule-preset-recommend --save-dev

Or with yarn:

yarn add --dev secretlint @secretlint/secretlint-rule-preset-recommend

If you'd rather not touch Node at all, Secretlint also ships as a Docker image and as a standalone single-executable binary you can download from the releases page. The Docker image even bundles the recommended preset, the custom pattern rule, and the SARIF formatter, so it's ready to scan straight away:

docker run -v `pwd`:`pwd` -w `pwd` --rm -it secretlint/secretlint secretlint "**/*"

Note the Node requirement if you go the npm route: current Secretlint expects Node.js 22 or newer.

Your First Scan

Once installed, generate a starter config and run a scan across your project.

npx secretlint --init
npx secretlint "**/*"

The --init step writes a .secretlintrc.json wired up to the recommended preset. The minimal version looks like this:

{
  "rules": [
    {
      "id": "@secretlint/secretlint-rule-preset-recommend"
    }
  ]
}

That single preset bundles the rules most teams need out of the box. The scan command takes a glob, and the exit code tells the story: 0 means clean, 1 means secrets were detected, and 2 means something went genuinely wrong. That 1 is what makes Secretlint useful as a gate — a failing scan fails the build.

Out of the box, files matched by your .gitignore are skipped (a default introduced in v13), as are anything listed in a .secretlintignore file. If you ever need to scan the ignored files too, pass --no-gitignore.

Tuning the Rules

A scanner is only as good as its signal-to-noise ratio, and Secretlint gives you several precise dials. Most rules accept an options object. A common need is allow-listing obvious dummy or example values so they stop tripping alarms:

{
  "id": "@secretlint/secretlint-rule-example",
  "options": {
    "allows": ["/dummy_secret/i"]
  }
}

If you'd rather silence a specific finding by its message id rather than by pattern, use allowMessageIds:

{
  "id": "@secretlint/secretlint-rule-example",
  "allowMessageIds": ["EXAMPLE_MESSAGE"]
}

When you're using a preset but want to tweak one rule inside it — say, allow a particular placeholder in the AWS rule — you nest the rule configuration under the preset's own rules key:

{
  "rules": [{
    "id": "@secretlint/secretlint-rule-preset-recommend",
    "rules": [{
      "id": "@secretlint/secretlint-rule-aws",
      "options": { "allows": ["xxxx-xxxx-xxxx"] }
    }]
  }]
}

For one-off exceptions in source, there's also inline suppression via the filter-comments rule. Wrap the offending region and Secretlint will look the other way:

// secretlint-disable
EXPOSED_SECRET_HERE
// secretlint-enable

A Rule Catalog That Knows Your Stack

The preset is a sensible baseline, but the real depth is in the service-specific rules — each its own npm package you can add as needed. There are dedicated detectors for AWS, GCP, Azure, GitHub, GitLab, Slack, Stripe, SendGrid, Shopify, OpenAI, Anthropic, Cloudflare, Vercel, Notion, Figma, HashiCorp Vault, 1Password, and many more. Beyond the cloud vendors, there are general-purpose rules for npm tokens, RSA and secp256k1 private keys, basic auth headers, and database connection strings.

When no off-the-shelf rule fits, the pattern rule lets you define your own regex right in the config — no custom package required:

{
  "rules": [{
    "id": "@secretlint/secretlint-rule-pattern",
    "options": {
      "patterns": [{
        "name": "Internal Service Token",
        "pattern": "/INT_[A-Z0-9]{32}/"
      }]
    }
  }]
}

This is the part that makes Secretlint feel like a platform rather than a fixed tool: your detection logic is just configuration or, when you outgrow that, a publishable package.

Wiring It Into Pre-Commit

The highest-value place to run Secretlint is the moment just before a commit is created. For Node projects, husky plus lint-staged is the natural home. Once both are set up, your package.json needs a single entry:

{
  "lint-staged": {
    "*": ["secretlint --no-glob"]
  }
}

That --no-glob flag is not optional here, and it's the one gotcha worth remembering. lint-staged passes literal file paths to the command, and those paths can contain characters that Secretlint would otherwise try to interpret as glob patterns. --no-glob tells it to treat each argument as an exact file path, which is exactly what you want when something else is already choosing the files.

If you work across languages and prefer the Docker-based pre-commit framework, Secretlint plugs in there too:

- repo: local
  hooks:
  - id: secretlint
    name: secretlint
    language: docker_image
    entry: secretlint/secretlint:latest secretlint

Guarding the Pull Request in CI

Local hooks are a great first line, but they can be skipped with --no-verify, so a CI check is the backstop. A full-project scan in GitHub Actions is a handful of lines:

name: Secretlint
on: [push, pull_request]
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 22
      - run: npm ci
      - run: npx secretlint "**/*"

The detail that makes this genuinely pleasant is the github formatter. Swap it in and Secretlint emits Actions workflow commands, which surface findings as inline annotations directly on the offending lines in the PR:

- run: npx secretlint --format github "**/*"

For large repositories where a full scan on every PR is wasteful, you can pair Secretlint with a changed-files action and scan only what moved — remembering --no-glob since you're handing it explicit paths:

- uses: tj-actions/changed-files@v44
  id: changed-files
- run: npx secretlint --no-glob ${{ steps.changed-files.outputs.all_changed_files }}

For teams that prefer a SARIF feed into a security dashboard, the @secretlint/secretlint-formatter-sarif formatter produces output that platforms like GitHub's code-scanning view ingest directly.

A Quirk Worth Knowing

Secretlint's semantic versioning has an unusual wrinkle that's worth internalizing. A minor version bump can newly fail a build that previously passed — not because anything broke, but because the new release added rules or detection capability that catches secrets it used to miss. That's a feature: your coverage improved. But it means pinning Secretlint's version in CI is a deliberate choice, not laziness. You want upgrades to be intentional, reviewed moments rather than surprises mid-sprint.

A Tidy Party Trick

Because Secretlint can rewrite files through the mask-result formatter, it doubles as a scrubber. Suppose you want to share a shell history file without leaking anything in it:

secretlint .zsh_history --format=mask-result --output=.zsh_history

It scans the file, masks any detected secrets in place, and writes the cleaned version back. Small, but the kind of thing you reach for more often than you'd expect.

Where It Fits

Secretlint is the natural choice for JavaScript and TypeScript teams who already lean on ESLint, Prettier, husky, and lint-staged — it joins that pipeline with no new mental model. It deliberately doesn't try to scan your entire git history, and it doesn't verify whether a found key is live against a vendor's API. Tools like Gitleaks (fast, Go-based, scans full history offline) and TruffleHog (verifies credentials against real endpoints) shine there. The mature pattern many teams settle on is defense in depth: Secretlint running locally and in PR checks to stop new secrets, with a history-aware scanner sweeping the repo in CI.

If your team has ever spent an afternoon rotating a leaked key, the few minutes it takes to wire Secretlint into your pre-commit hook is among the highest-leverage security investments you can make. It's quiet, it's configurable, and it keeps the worst kind of mistake from ever leaving your laptop.