Skip to main content
Glama
README.md
# aftertaste

**Find what your AI coding agent left behind.**

[![npm](https://img.shields.io/npm/v/aftertaste?color=cb3837)](https://www.npmjs.com/package/aftertaste)
[![CI](https://github.com/WiseeCarrot/aftertaste/actions/workflows/ci.yml/badge.svg)](https://github.com/WiseeCarrot/aftertaste/actions/workflows/ci.yml)
[![dependencies: 0](https://img.shields.io/badge/dependencies-0-3fb950)](package.json)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![aftertaste](docs/badge.svg)](#badge)

Agents write a lot of code very quickly, and some of what they leave behind compiles fine and still breaks things: an import of a package that doesn't exist, an OpenAI key in a `NEXT_PUBLIC_` variable, `// ... existing code ...` where your function used to be, a `describe.only` that quietly switched off the rest of your tests. Linters don't look for this kind of thing. aftertaste does.

```bash
npx aftertaste
```

There's nothing to install or configure and no account to make. It has zero dependencies and takes about a second on most repos. It understands JavaScript, TypeScript and Python projects, and checks for secrets and leftovers in Go, Rust, Java, C#, Ruby and shell too.

<img src="docs/demo.svg" alt="aftertaste output for a sample Next.js app: hallucinated packages, an exposed OpenAI key, a Supabase service_role key, truncated code, a merge conflict and a focused test" width="100%">

<sub>That's the real output for [`examples/vibe-app`](examples/vibe-app), a small Next.js app with the usual problems. Try it yourself: `npx aftertaste examples/vibe-app`.</sub>

## What it catches

| | Rule | What it finds |
|---|---|---|
| **Leaks** | [`hardcoded-secret`](docs/rules.md#hardcoded-secret) | API keys and tokens for 25+ providers, private keys, database URLs with passwords |
| | [`client-exposed-secret`](docs/rules.md#client-exposed-secret) | `NEXT_PUBLIC_` / `VITE_` secrets, `dangerouslyAllowBrowser: true`, Supabase `service_role` keys in client code |
| | [`gitignore-gap`](docs/rules.md#gitignore-gap) | `.env` and `node_modules` that are committed, or one `git add .` away from it |
| **Broken builds** | [`hallucinated-package`](docs/rules.md#hallucinated-package) | Imports and dependencies that don't exist on npm or PyPI, plus brand-new lookalike packages |
| | [`undeclared-dependency`](docs/rules.md#undeclared-dependency) | Imports that work on the agent's machine but aren't in `package.json` / `requirements.txt` |
| | [`truncated-code`](docs/rules.md#truncated-code) | `// ... existing code ...`, `# rest of the file unchanged` and other elisions that deleted real code |
| | [`merge-conflict`](docs/rules.md#merge-conflict) | Leftover `<<<<<<<` / `>>>>>>>` blocks |
| | [`focused-test`](docs/rules.md#focused-test) | `it.only`, `describe.only` and `fit`, which skip every other test without failing CI |
| **Shortcuts** | [`unsafe-code`](docs/rules.md#unsafe-code) | `eval`, TLS checks turned off, SQL built from template strings, `Math.random()` tokens, `*` CORS with credentials |
| | [`unfinished-stub`](docs/rules.md#unfinished-stub) | "In a real app you would...", mock data, simulated payments, `YOUR_API_KEY` placeholders, `throw new Error("Not implemented")` |
| | [`swallowed-error`](docs/rules.md#swallowed-error) | `catch {}`, `.catch(() => {})` and `except: pass` |
| **Mess** | [`stray-file`](docs/rules.md#stray-file) | `utils_old.py`, `App copy.tsx`, `api_v2.ts` sitting next to `api.ts`, `.bak` files |
| | [`debug-leftover`](docs/rules.md#debug-leftover) | `debugger`, `breakpoint()`, `pdb.set_trace()`, `dbg!`, stray `console.log` |
| | [`duplicate-block`](docs/rules.md#duplicate-block) | The same helper copied into several files |
| | [`localhost-url`](docs/rules.md#localhost-url) | `http://localhost:3000` hardcoded into app code |
| | [`undocumented-env`](docs/rules.md#undocumented-env) | Env vars the code reads that `.env.example` never mentions |
| | [`unused-dependency`](docs/rules.md#unused-dependency) | Packages installed for an approach the agent later abandoned |
| | [`ai-chatter`](docs/rules.md#ai-chatter) | "Here's the updated code", "I've added...", `// <- fixed` |
| | [`oversized-file`](docs/rules.md#oversized-file) | Files that have grown past what an agent can edit safely |

Each rule has a page in [docs/rules.md](docs/rules.md) covering why it matters and how to fix it. You can also run `npx aftertaste explain <rule>`.

## Hallucinated packages

Models make up package names that sound right. A [USENIX Security 2025 study](https://www.usenix.org/conference/usenixsecurity25/presentation/spracklen) looked at 576,000 generated code samples and found that about 5.2% of the packages commercial models recommended didn't exist, and about 21.7% for open-source models. Attackers know this. They register those names and wait for someone to `npm install` them, which is called *slopsquatting*.

So aftertaste checks every import and every declared dependency against the registry:

- **Doesn't exist** on npm or PyPI → `error`, with the file and line where it's used
- **Exists but is new and barely used** (under 60 days old, under 100 downloads a week) → `warning`, because that's what a squatted name looks like
- **Exists but isn't declared** → `error`, since a fresh install will crash

Only package names go to the registries; your code never does. Packages that exist are cached for a week in `~/.cache/aftertaste`, or wherever `AFTERTASTE_CACHE_DIR` points, so repeat runs make almost no requests. Scopes from `.npmrc` with a private registry are never looked up. Pass `--offline` to skip the network entirely. You still get undeclared imports reported, just without confirmation from the registry.

## Make your agent check its own work

### Claude Code

```bash
npx aftertaste init --claude
```

This adds two things:

- a **Stop hook**. When Claude says it's done, aftertaste scans the files it changed. If it finds errors, Claude gets the list and keeps working. It only nudges once per stop, so it can't loop.
- the **MCP server**. Claude can call `aftertaste_check_package` before installing anything and `aftertaste_scan` after a change.

<details>
<summary>Do it by hand</summary>

`.claude/settings.json`:

```json
{
  "hooks": {
    "Stop": [{ "hooks": [{ "type": "command", "command": "npx --yes aftertaste@1 --agent-hook", "timeout": 120 }] }]
  }
}
```

`.mcp.json`:

```json
{ "mcpServers": { "aftertaste": { "command": "npx", "args": ["--yes", "aftertaste@1", "mcp"] } } }
```

</details>

### Cursor, Windsurf, Cline, Zed, or any MCP client

Add the same server to the client's MCP config:

```json
{ "mcpServers": { "aftertaste": { "command": "npx", "args": ["--yes", "aftertaste@1", "mcp"] } } }
```

| Tool | What it does |
|---|---|
| `aftertaste_scan` | Scans the project (or only changed files) and returns the findings as a fix list |
| `aftertaste_check_package` | Confirms a package exists on npm/PyPI and flags suspicious new ones, before the agent installs it |

### Any chat window

```bash
npx aftertaste -f prompt | pbcopy     # Windows: | clip
```

Paste the result into any agent. It's a prioritized list grouped by rule, with fix instructions for each one.

## In CI and git hooks

### GitHub Actions

```yaml
# .github/workflows/aftertaste.yml   (or: npx aftertaste init --github)
name: aftertaste
on: [pull_request, push]
permissions:
  contents: read
jobs:
  aftertaste:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: WiseeCarrot/aftertaste@v1
```

Findings show up as annotations on the PR diff and as a table in the job summary. The action takes these inputs: `path`, `fail-on` (default `error`), `min-score`, `offline`, `sarif` (a file path, for GitHub code scanning) and `args`.

<details>
<summary>Upload to GitHub code scanning (SARIF)</summary>

```yaml
permissions:
  contents: read
  security-events: write
steps:
  - uses: actions/checkout@v4
  - uses: WiseeCarrot/aftertaste@v1
    with:
      sarif: aftertaste.sarif
      fail-on: never
  - uses: github/codeql-action/upload-sarif@v3
    with:
      sarif_file: aftertaste.sarif
```

</details>

### Pre-commit

```bash
npx aftertaste init --hook        # plain git hook: checks staged files only
```

Or with the [pre-commit](https://pre-commit.com) framework:

```yaml
repos:
  - repo: https://github.com/WiseeCarrot/aftertaste
    rev: v1.0.0
    hooks:
      - id: aftertaste
```

### Other CI

```bash
npx aftertaste --since origin/main -f github      # GitHub-style annotations
npx aftertaste -f sarif:aftertaste.sarif          # SARIF file
npx aftertaste -f json                            # everything, machine readable
```

## Badge

```bash
npx aftertaste --badge docs/aftertaste.svg
```

That writes a local SVG badge (no third-party service) and prints a shields.io link if you'd rather use that:

```markdown
[![aftertaste](docs/aftertaste.svg)](https://github.com/WiseeCarrot/aftertaste)
```

The score starts at 100. Each rule that fires costs `15` for an error, `5` for a warning and `1` for info, multiplied by `1 + log2(count)`. A rule that fires many times still counts, but one noisy rule can't sink the whole score. Grades: A ≥ 90, B ≥ 80, C ≥ 70, D ≥ 60.

## Configuration

Everything is optional. Add `aftertaste.config.json` (or run `npx aftertaste init`):

```json
{
  "$schema": "https://raw.githubusercontent.com/WiseeCarrot/aftertaste/main/schema.json",
  "ignore": ["legacy/", "**/*.generated.ts"],
  "failOn": "error",
  "minScore": 80,
  "rules": {
    "debug-leftover": "off",
    "localhost-url": "warning",
    "oversized-file": ["info", { "maxLines": 1500 }],
    "duplicate-block": ["warning", { "minLines": 12 }],
    "unused-dependency": ["info", { "ignore": ["@fontsource/inter"] }]
  }
}
```

You can also put the same object under an `"aftertaste"` key in `package.json`. Files ignored by `.gitignore` are always skipped.

**Silencing a single finding.** Put a comment on the line, or the line above. Naming the rule is optional, and anything after `--` is a note for whoever reads it next:

```js
const sql = `SELECT * FROM ${table}`; // aftertaste-ignore-line unsafe-code -- table name comes from an allowlist

// aftertaste-ignore-next-line debug-leftover
console.log(report);

// aftertaste-ignore-file truncated-code   (anywhere in the file)
```

## CLI

```text
aftertaste [path | files...]     scan a project (default: current directory)
aftertaste init [--hook] [--github] [--claude]
aftertaste rules                 list every rule
aftertaste explain <rule>        what a rule catches and how to fix it
aftertaste mcp                   run as an MCP server

--staged / --changed / --since <ref>   only report on changed files
--rule <id>                            run only this rule (repeatable)
-f, --format <name>[:file]             pretty, json, sarif, github, markdown, prompt (repeatable)
--badge <file.svg>                     write a score badge
-q, --quiet                            hide info-level findings
--fail-on <error|warning|info|never>   exit 1 at this level (default: error)
--min-score <n>                        also exit 1 below this score
--offline                              never contact npm or PyPI
--fix                                  add missing .gitignore entries
```

Exit codes: `0` clean, `1` findings at or above `--fail-on` (or score below `--min-score`), `2` usage or config error.

## How it works, and what it won't do

- **It aims for precision over recall.** A tool that cries wolf gets uninstalled, so each rule only fires on patterns that are almost never intentional. Explained ignores (a comment inside the `catch`, `pass  # best effort`) aren't flagged, and neither are test files, mocks, seeds or stories where fakes are the point. This repository runs aftertaste on itself in CI with `failOn: "info"`.
- **It's aware of comments and strings.** A small lexer splits every file into code, comments and strings. That way `"eval("` in an error message isn't an `eval`, and a comment that *quotes* `"... existing code ..."` isn't an elision.
- **It never prints a whole secret.** Reports show `sk-p…Xq (164 chars)`.
- **It isn't a replacement** for your type checker, ESLint/Ruff, a full secret scanner like gitleaks, or a dead-code tool like knip. It covers the gaps between them that agent-written code tends to fall into, and runs alongside all of them.

## API

```js
import { scan, prompt } from 'aftertaste';

const result = await scan({ root: '.', online: false });
console.log(result.score, result.grade, result.findings);
console.log(prompt(result)); // the agent-ready fix list
```

## Contributing

False positives are bugs. Please [open an issue](https://github.com/WiseeCarrot/aftertaste/issues/new?template=false-positive.yml) with the smallest snippet that triggers it. New rules are welcome too: they need a test that proves they fire, and one that proves they stay quiet on ordinary code. See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

[MIT](LICENSE)