What it is, in one minute
The five things it checks
- Secrets. An API key, a password or a private key pasted into the code. It also reads committed .env files, and knows the difference between a real key and a placeholder like ${DB_PASSWORD}.
- Tenant scope. In an app where many customers share one database, a query that forgets to filter by customer shows one customer another customer's data. This gate catches list-style queries on tenant-owned tables that never mention the tenant column.
- Money. Math.round(price * 100) and parseFloat(amount) are the two classic ways to lose cents. Floats cannot hold money exactly.
- Migration safety. A new database migration that adds a NOT NULL column with no default, or renames a column, breaks the version of the app that is still running while the deploy happens.
- Doc links. A link in a README that points at a file someone deleted last month.
Where it runs
- In CI. A GitHub Action, uses: ajeermahmood/bouncer-gates@v0, blocks the merge and can post findings to the GitHub Security tab.
- In your editor, next to the AI agent. This is the newest part. CI sees a mistake after the pull request exists. An AI coding editor like Claude Code or Cursor can see it in the same turn the mistake was made. npx bouncer-gates --init-agents wires the gates in as an MCP server and a post-edit hook, so the agent gets told "this query is not tenant scoped" the moment it writes the line, not twenty minutes later in a red CI check.
- In a Cloudflare Worker and on Railway. The playground on the site sends your snippet to the worker, and a hosted API on Railway serves teams that want the checks over HTTP without running Node.
- In the browser. If the worker is unreachable, the playground runs the same gate code locally and the footer says so.
Getting started on an existing codebase
The number that mattered was the false positives
- Test fixtures. Fake credentials inside .spec.ts files were a third of all findings, and every one was on purpose. They now report as warnings, not blocks. One exception stays blocking: an sk_live_ Stripe key in a test file is not plausible fake data.
- Percentages. Math.round(x * 100) is both the classic money bug and the normal way to render a percentage. The tell is a division inside the rounded expression. A percentage is a part over a whole. Money conversion never divides before it scales.
- Local dev credentials. postgres:postgres@localhost in a setup script's help text grants nothing to anyone.