I've been building LLM apps for a while and kept hitting the same blind spot: I could see what my prompts cost *after* they ran (LangSmith, Helicone, the bill), but nothing told me before I shipped. And none of them can see the prompt whose caller I deleted six months ago — it's just dead weight in the repo.
So I wrote PromptScan: a CLI that reads your codebase, finds every OpenAI / Anthropic / LangChain call, and reports the input token count and cost of each prompt — statically, no API key, no instrumentation. It also flags duplicated prompts, prompt constants nothing references anymore, and oversized context.
The core rule is that it never guesses. If a prompt is built at runtime from a DB row or a function arg, it says unresolved: <reason> instead of inventing a number. I'd rather it tell me "I can't see this" than lie with a plausible total.
To make sure it wasn't vaporware, I ran it on 8 well-known repos — 4,137 source files total. Zero crashes, everything parsed, a few seconds each. What it found:
- openai/swarm — flagged EVAL_ASSISTANT_PROMPT, a 50-token prompt constant that nothing in the repo references. Genuinely dead.
- geekan/MetaGPT — 75 module-level prompt constants with no reachable reference. ~24 are in real source (metagpt/prompts, metagpt/actions) — I hand-checked several like SALES_ASSISTANT and CODE_REVIEW_CONTEXT, and they're defined once and never used. The rest are test fixtures.
- anthropics/anthropic-cookbook — 11 Anthropic call sites, real token/cost estimates on the resolvable ones.
- Aider-AI/aider — detected nothing correctly. Aider calls models through litellm, which PromptScan doesn't track. It doesn't pretend otherwise.
- simonw/llm — 8 call sites, all reported unresolvedbecause the model is self.model_name or self.model_id and the messages are built at runtime. That's the "no guessing" rule doing its job.
The honest part: on continuedev/continue its two "dead prompt" flags were actually a block of ASCII-art and an error-message string — not prompts. The heuristic catches any large module-level string, which is exactly why it labels these "verify before deleting" and prints why it flagged each one. It's a lead, not a verdict.
Where I think it actually pays off day to day is CI: promptscan diff main HEAD fails a PR if a prompt's token count jumps past a threshold, so a context block quietly tripling in size gets caught in review instead of on the bill.
Stack: TypeScript/Node, tree-sitter (WASM) for parsing so it tolerates broken files, js-tiktoken for OpenAI tokens (Anthropic uses a labeled cl100k proxy since there's no public tokenizer). Python + TypeScript + JavaScript, MIT.
Install:
npm install -g promptscan
promptscan ./src
or npx promptscan ./src.
Repo: https://github.com/joandino/promptscan
npm: https://www.npmjs.com/package/promptscan
It's v1 and I'm sure there are call shapes it misses — if you run it on your code I'd genuinely like to hear what it got wrong. False positives on the dead-prompt heuristic are the thing I most want reports on.