India
ProjectsAugust 31, 2026 · Updated September 20, 2026

bouncer-gates, a safety check for your codebase that anyone can pass

JavaScript
Astro
Cloudflare Workers
GitHub Actions
MCP
Vitest
bouncer-gates, a safety check for your codebase that anyone can pass screenshot
More people can write code now than a year ago. A designer can ship a page, a support lead can fix their own copy, and an AI agent can open twenty pull requests before lunch. Code review did not get faster to match. So the hard part moved: it is no longer writing the change, it is knowing the change is safe. bouncer-gates is a small tool that stands at the door of your main branch and checks every change for a short list of mistakes that are expensive when they reach production. If it finds one, it tells you the file, the line, and what to do instead. You do not need to already know that the orders table is tenant-scoped, or that you cannot store money in a float. The tool knows, and it tells you. Try it in ten seconds: The playground runs the same checks on any code you paste in. It is MIT licensed, on npm as bouncer-gates, and listed in the official MCP Registry as io.github.ajeermahmood/bouncer-gates, so any MCP client can install it in one step. The rules come from the checks I run on a multi-tenant e-commerce platform where most of the code is written with a coding agent. How that workflow fits together is written up separately. The longer story of why checks beat instructions is in Making a codebase safe for AI coding agents. Each check is called a gate. A gate either lets a change through or stops it with a reason.
  1. 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}.
  2. 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.
  3. Money. Math.round(price * 100) and parseFloat(amount) are the two classic ways to lose cents. Floats cannot hold money exactly.
  4. 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.
  5. Doc links. A link in a README that points at a file someone deleted last month.
That is the whole list on purpose. Anything a formatter, a linter or a compiler can already check does not belong here. The same five gates run in four places, and it is the same code in all four:
  • 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.
Each gate is a plain function: files in, findings out. No filesystem, no git, no environment variables, no printing. All of that lives in one runner. That one decision is why the same code can run everywhere without four copies of the rules drifting apart. Two commands: If the repository has a Prisma schema, --init reads it, finds every model with a tenantId field, and fills in the tenant scope config for you. It guesses the tenant column from whatever owner-shaped field appears on the most models, so a schema built on storeId works too. The doc-links repo URL comes from your git remote. An old codebase will already have things wrong in it. Nobody fixes two hundred findings before merging anything else, so --baseline-write records what is already there. Old findings stop blocking, new ones block right away, and the debt stays visible and countable. If a gate is wrong about one specific line, you tell it so on the line: The reason is required. A bare marker suppresses nothing. That way the excuse lives in the file where the next reader will find it, not in a pull request nobody will open again. There is deliberately no switch to turn a gate off across the whole repository. If a gate is wrong that often, the gate is what needs fixing. Anyone can write a checker that catches the bad case. The hard part is not crying wolf. I pointed an early build at a real 1,209 file production monorepo. It reported 45 blocking findings. I read all 45 by hand, and 32 were wrong. At that rate a team stops reading the output inside a month, and a check nobody reads is worse than none, because it still looks like protection. The false positives came in three families:
  • 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.
After fixing those, the same repository reports 13 findings and all 13 are real. Every one of the 32 false positives is now a test that asserts the gate stays quiet, named for what the mistake actually was. The suite is at 159 tests, and the most valuable ones are the ones that document a mistake the tool really made. "We looked and it was fine" and "we could not look" are different answers, and only one of them is safe to merge on. The migration gate compares against the base branch to find new .sql files. On a shallow CI clone there is no base branch, so nothing looked new, so the gate printed a reassuring "no new migrations" and would have printed it forever. Now the skip reason is a sentence, not a boolean, so those two cases cannot look the same: The same rule runs through everything. A gate that throws is a failure, not a silence. A typo in a command-line flag exits with an error instead of being ignored and quietly scanning nothing. Inside the editor, a gate that could not run is listed next to the findings, because an agent told "the code is clean" when the scope gate never ran would learn the wrong thing. Total scan time on that same repository went from 291ms to 73.6ms. Three fixes did almost all of it, and none of them were visible until measured: a regex was being compiled once per line per gate, roughly a million times. The scope gate rebuilt a matcher inside its inner loop. And each gate now tests one combined pattern against the whole file before looking at any line, because almost every line contains nothing interesting. Worker threads were considered and rejected. At 73ms, starting a thread pool costs more than the work. It is not a replacement for gitleaks or trufflehog, which scan git history for secrets far more thoroughly. It is not Semgrep, which is a real static analysis engine. It is not ESLint, which owns everything about the shape of the code. Every gate documents what it misses, and those lists are the specification. The scope gate cannot follow a client passed into a transaction callback, which is the single most likely place a real cross-tenant leak survives a green build. It cannot know whether a written excuse is still true a year later. A tool that claims to catch everything teaches people to stop reading, and then the one it missed ships. One ping a day: a random id, the version, the runtime, the OS and the Node major. Nothing about your code. BOUNCER_TELEMETRY=0 or DO_NOT_TRACK=1 switches it off, the first run says so, and the full list of what it sends is in the repository.

Related projects

estate, a linter for your repositories

estate, a linter for your repositories

Checks every repository you own against declarative rules and exits non-zero, so the problems nobody notices for months fail a build instead. One Go binary, no dependencies, no dashboard.
Enrixa Store, a multi-tenant platform built with AI agents

Enrixa Store, a multi-tenant platform built with AI agents

A multi-tenant e-commerce platform built mostly by a coding agent: 1,276 of my 1,357 commits were written with Claude Code, and every push runs gates that fail the build on a query that could leak one merchant's data into another store.