npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cachelint

v0.1.1

Published

A linter + CI gate for LLM prompt-cache regressions. Catches the silent-invalidator class (interpolated timestamps, unsorted JSON, BOM, mixed line endings) and fails PRs when a committed prompt's stable-prefix hash moves.

Downloads

325

Readme

cachelint

A linter + CI gate for LLM prompt-cache regressions.

Anthropic and OpenAI prompt caching are prefix matches: a single byte change anywhere in the cacheable prefix invalidates the cache. Common causes — an interpolated {{ now }} in a system prompt, an unsorted JSON.stringify, a leading BOM, mixed line endings — sail through code review and quietly 10–20× your token cost in production. It's documented in Anthropic's own bug tracker:

cachelint does one thing: it catches your own committed prompt content silently breaking the cacheable prefix — at edit time and in CI — before it ships. It is zero-cost to run (no servers, no paid APIs, no telemetry).

Install

npm i -g cachelint          # or: npx cachelint …

ESM-only, Node ≥ 22. Ships a cachelint binary and a library (import { lint, hash, check } from "cachelint").

Quickstart

  1. Lint your prompt files for known silent-invalidator patterns:

    cachelint lint prompts/system.md prompts/tools.json
  2. Hash them and commit the lockfile (like package-lock.jsonhash writes, check verifies):

    cachelint hash prompts/system.md prompts/tools.json   # writes cachelint.lock
    git add cachelint.lock && git commit -m "lock prompt-cache prefixes"
  3. Check in CI — fails the build if a committed prompt's stable-prefix hash moved without a fresh cachelint hash:

    cachelint check

That's it. If check fails and the change was intentional, re-run cachelint hash … and commit the updated cachelint.lock.

Optional config

cachelint.config.json (or .yaml / .yml — data only, never executable):

{
  "$schema": "https://raw.githubusercontent.com/davioe/cachelint/main/schema/cachelint.config.schema.json",
  "prompt_globs": ["prompts/**/*.md", "prompts/**/*.json"],
  "bundles": [
    { "name": "system", "files": ["prompts/system.md", "prompts/tools.json"] }
  ],
  "disable": [],                 // e.g. ["R003"]
  "rules": { "R001": "error" }   // override default severities; "off" disables
}

With prompt_globs set you can just run cachelint lint / cachelint hash / cachelint check with no arguments.

Suppressing a finding

Inline, ESLint-style — cachelint-disable-next-line works in any comment style:

<!-- cachelint-disable-next-line R001 -->
The treaty was signed on 1989-11-09T18:53:00Z.

GitHub Action

# .github/workflows/cachelint.yml
permissions:
  contents: read
  # security-events: write   # only if you set `sarif:` below (Pro)
jobs:
  cachelint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: davioe/cachelint@<commit-sha>   # pin to a SHA, not a tag
        with:
          paths: 'prompts/**/*.md prompts/**/*.json'
          # license: ${{ secrets.CACHELINT_LICENSE_KEY }}   # optional, for Pro

The action masks the license input, installs an exact cachelint version (never a floating tag), runs lint then check, and (Pro) uploads SARIF for PR annotations. Pin the action itself to a commit SHA, not a moving tag.

CI job summary

On GitHub Actions — and on any CI that exposes a GITHUB_STEP_SUMMARY file path — cachelint lint and cachelint check each append a titled cache-health section (findings table, bundle stability, cost estimates) to the job summary, even on thrown errors. It is best-effort and purely cosmetic: it never affects exit codes, respects --no-cost, redacts secrets, and is a silent no-op where the env var is absent. Opt out by setting CACHELINT_NO_STEP_SUMMARY to any non-empty value — e.g. CACHELINT_NO_STEP_SUMMARY=1 (or the action's no-step-summary: "true" input).

Fork PRs on public repos

Fork PRs don't get repo secrets, so a license-gated step would fail on them. If you use a Pro feature, gate just that and let the free lint + check (which need no secret) run everywhere:

      - uses: davioe/cachelint@<sha>
        continue-on-error: ${{ github.event.pull_request.head.repo.fork }}
        with:
          paths: 'prompts/**/*'
          license: ${{ secrets.CACHELINT_LICENSE_KEY }}
          sarif: cachelint.sarif

(The free tier alone catches the whole silent-invalidator class; Pro adds the SARIF annotations on top.)

README badge (once published)

Once the repo is public, advertise the gate with a static shields.io badge — swap in your OWNER/REPO and workflow file name:

[![cachelint](https://img.shields.io/github/actions/workflow/status/OWNER/REPO/cachelint.yml?label=cachelint)](https://github.com/OWNER/REPO/actions/workflows/cachelint.yml)

Or the label-only variant if you don't run the workflow on every push:

[![prompt cache: protected](https://img.shields.io/badge/prompt_cache-protected-brightgreen)](https://github.com/davioe/cachelint)

Pre-commit hooks

The repo ships .pre-commit-hooks.yaml with two hooks for the pre-commit framework — cachelint (lint) and cachelint-check (the lockfile gate). (Once published — the repo: URL goes live when the repo does.)

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/davioe/cachelint
    rev: v0.1.0 # pin to a release tag
    hooks:
      - id: cachelint
      - id: cachelint-check

Both hooks run config-scoped on the whole project (pass_filenames: false, always_run: true) — deliberate: check must see the full bundle set (passed filenames would redefine the bundles), and lint stays scoped to prompt_globs so R003 behaves exactly like CI.

What gets caught

| Rule | Pattern | Severity | Why it matters | Docs | | ----- | ---------------------------------------------------------------------- | -------- | --------------------------------------------------- | ---- | | R001 | Interpolated timestamp ({{ now }}, Date.now(), datetime.now(), "Current Date & Time:", ISO-8601 datetimes) | warn | Prefix bytes change every call → 100% cache miss after that point | R001 | | R002 | Interpolated UUID / random (crypto.randomUUID(), Math.random(), uuidv4(), nanoid(), {{ requestId }}, …) | warn | Same — non-determinism in the prefix | R002 | | R003 | Unsorted JSON.stringify (no key-sort replacer) — only on files in prompt_globs / cachelint.lock / named literally | warn | Object key order isn't guaranteed stable → cache thrash | R003 | | R004 | Leading UTF-8 BOM | error | An invisible char at the head of the prompt; cross-OS checkouts introduce it | R004 | | R005 | Mixed CRLF / LF / CR line endings in one file | error | Hashes differently on Windows vs Linux even though the text "looks" identical | R005 |

cachelint hash / check normalize line endings + strip the BOM before hashing, so the gate isn't fooled — but the lint rules still flag the source so the inconsistency gets fixed. Pre-launch the false-positive rate of R001–R003 is audited against real OSS repos and published in each rule's page.

Output

  • Human (default, on stderr): file:line:col level RID message, plus a fix hint and a docs link per finding.
  • --json (on stdout): a stable schema — { version, configPath, diagnostics, warnings, exitCode }, plus an optional cost block (absent under --no-cost).
  • --sarif <path> (Pro): SARIF v2.1 for GitHub Code Scanning.

Cost estimates

Findings carry a deliberately conservative dollar estimate of what the broken prefix costs you (full recipe, assumptions, and error bounds: docs/cost-model.md). Per finding, cachelint lint prints:

prompts/random.md:1:13  warning  R002  `crypto.randomUUID()` in prompt content — a fresh UUID every call
  › Session id: crypto.randomUUID()
  fix: Generate the id outside the cached prefix (or after the last breakpoint).
  why: https://github.com/davioe/cachelint/blob/main/docs/rules/R002.md
  → invalidates ~8 stable prefix tokens from here
  ≈ $0.022 wasted per 1,000 calls (est. if caching is enabled, claude-sonnet-4-6 pricing, 2026-06)

…and a run summary:

cachelint: 5 problems (2 errors, 3 warnings)
cachelint: est. waste ≈ $0.068 per 1,000 calls · ~19 prefix tokens at risk (est. if caching is enabled, claude-sonnet-4-6 pricing, 2026-06)

A failing cachelint check prints the waste/at-risk totals before its verdict (and a green check prints a value receipt of what the stable prefixes protect):

cachelint: cachelint.lock is out of date —
~ bundle "prompts/a.md" stablePrefixHash moved:
    was: 70a6822d8636ba2c31494cda7700b158cf426837df5fe6515ba228d817615f15
    now: ccbaa0ede490b50c246fb4f98e227968653eff0f5a8f666d1fba78ae6cb33d87
    prompts/a.md: 6cdf29393117 → 6a02769c47b4

If this change is intentional: run `cachelint hash …` and commit the updated cachelint.lock.
prompts/a.md:2:13  warning  R001  template helper injects the current date/time into the prompt
  why: https://github.com/davioe/cachelint/blob/main/docs/rules/R001.md
  → invalidates ~3 stable prefix tokens from here
prompts/b.md:1:1  error    R004  file begins with a UTF-8 BOM (U+FEFF) — an invisible character at the head of the prompt
  why: https://github.com/davioe/cachelint/blob/main/docs/rules/R004.md
  ~4 prefix tokens at risk (byte-stable — changes on edit/regeneration, not per call)
cachelint: est. waste ~3 stable prefix tokens per call (under $0.01 per 1,000 calls) · ~4 prefix tokens at risk
cachelint: FAILED — stable-prefix hash moved; 1 error-severity finding

Cost output is cosmetic — it never affects exit codes, hashing, or the lockfile — and --no-cost removes it from every surface (human, --json, CI summary). See docs/cost-model.md for why the numbers are defensible: volatility classes, the earliest-divergence model, and every conservatism guarantee.

Exit codes (v1.0 stability contract)

| code | meaning | | ---- | ------- | | 0 | ok (or only warn-severity findings without --strict) | | 1 | a stablePrefixHash moved (regression), or an error-severity rule fired (or a warn under --strict) | | 2 | a Pro feature (--sarif, --exact, --pack) was invoked without a license | | 3 | cachelint.lock is missing or corrupt | | 4 | config or source error |

OSS vs Pro

| | Free (MIT) | Pro | | --- | --- | --- | | cachelint lint (all rules, pragmas, --json) | ✅ | ✅ | | cachelint hash / check — single and multi-file bundles | ✅ | ✅ | | The GitHub Action | ✅ | ✅ | | --sarif PR annotations (GitHub Code Scanning¹) | — | ✅ | | --exact token counting (Anthropic count-tokens API, your key) | — | ✅ | | Provider bug-pattern packs (--pack anthropic-2026-q1) | — | ✅ |

¹ Uploading SARIF to a private repo requires GitHub Advanced Security; on public repos it works on the free plan. Disclosed in LICENSE-PRO.md.

Pro is a one-time individual / annual team license. Keys are offline-verifiable (Ed25519 via node:crypto) — no servers, no telemetry, works in air-gapped CI. cachelint activate <key> to enable, cachelint license status to inspect, cachelint deactivate to remove; CACHELINT_LICENSE_KEY env for CI. The buyer's email is embedded in the key and printed by cachelint --version — the only anti-share signal, disclosed at checkout. See docs/licensing.md.

Design contract

  • Zero operating cost. No servers. No paid APIs (--exact uses your ANTHROPIC_API_KEY). No telemetry, ever.
  • Determinism. cachelint.lock is byte-identical across Windows / macOS / Linux and Node 22/24. Full spec: docs/determinism-contract.md.
  • Config is data, not code. .json / .yaml only — no .ts/.js config, so a shared config can't execute anything.
  • Rules are pure functions (no I/O, no network) — enforced by a sandbox harness, so future community rule packs are safe by construction.
  • No secret ever leaks. API keys and license tokens are redacted in every output path, including --verbose.

License

Free core: MIT (LICENSE). Pro features: LICENSE-PRO.md.

Contributing

See CONTRIBUTING.md. Implementation plan: docs/plans/2026-05-11-002-feat-cachelint-mvp-plan.md.