Open source · MIT · runs offline · Laravel & NestJS
Your AI said it followed your architecture. Check.
NoBypass fails the pull request when generated code works but skips the layers your team approved — a query in a controller, a repository injected where only a service belongs, a catch that swallows everything.
npx nobypass init
Static analysis only. No LLM, no API key, no account, no upload.
User::where() in a controller works. It also skips three approved layers.Tests pass
The endpoint returns the right JSON. Nothing in the test suite cares which layer ran the query.
The linter passes
Formatting, types and unused imports are fine. Linters don't know your layering.
The architecture drifted
Ten agent sessions later, half the controllers talk to the database and the service layer is decoration.
Same feature, two versions
Real output from the fixtures in the repository. Switch the framework, then compare what the agent wrote with the fix.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreUserRequest;
use App\Models\User;
use App\Repositories\UserRepository;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function __construct(
private readonly UserService $service,
private readonly UserRepository $repository,
) {
}
public function index(): JsonResponse
{
// "Quick fix" from an AI agent: query straight from the controller.
$users = User::where('active', true)->orderBy('name')->get();
return response()->json($users);
}
public function store(StoreUserRequest $request): JsonResponse
{
return response()->json($this->service->register($request->validated()), 201);
}
}
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreUserRequest;
use App\Support\UserSupport;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function __construct(private readonly UserSupport $users)
{
}
public function index(): JsonResponse
{
return response()->json($this->users->listActive());
}
public function store(StoreUserRequest $request): JsonResponse
{
return response()->json($this->users->register($request->validated()), 201);
}
}
import { Body, Controller, Get, Post } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import type { UsersRepository } from './users.repository';
import { CreateUserDto } from './dto/create-user.dto';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(
private readonly users: UsersService,
private readonly prisma: PrismaService,
) {}
// "Quick fix" from an AI agent: read straight from Prisma in the controller.
@Get()
list() {
return this.prisma.user.findMany({ where: { active: true } });
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.users.register(dto);
}
describe(repo: UsersRepository) {
return repo.constructor.name;
}
}
import { Body, Controller, Get, Post } from '@nestjs/common';
import { UsersService } from '@app/users/users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly users: UsersService) {}
@Get()
list() {
return this.users.listActive();
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.users.register(dto);
}
}
$ npx nobypass check ✖ BND003 error app/Http/Controllers/UserController.php:14 controller depends directly on service: App\Services\UserService expected: service is reached only through controller → support → service fix: Call the support layer instead and let it reach service. ✖ BND001 error app/Http/Controllers/UserController.php:22 controller depends directly on model: App\Models\User ✖ BND002 error app/Http/Controllers/UserController.php:22 controller performs data access: Eloquent query User::where() expected: Data access only in repository, reached via controller → support → service → repository fix: Move the query into the repository layer. ⋮ 6 more findings in this fixture 9 files analyzed · 7 new errors · 2 new warnings · 0 known ✖ FAIL — 7 blocking findings.
$ npx nobypass check 9 files analyzed · 0 new errors · 0 new warnings · 0 known ✔ PASS — no new blocking architecture violations.
$ npx nobypass check ✖ BND001 error src/users/users.controller.ts:2 controller depends directly on persistence: src/prisma/prisma.service.ts expected: controller may depend directly on: service ✖ BND003 error src/users/users.controller.ts:3 controller depends directly on repository (type-only reference) expected: repository is reached only through controller → service → repository ✖ BND002 error src/users/users.controller.ts:17 controller performs data access: Direct ORM access through PrismaService (this.prisma.user.findMany) fix: Move the query into the repository layer. ⋮ 3 more findings in this fixture 8 files analyzed · 5 new errors · 1 new warning · 0 known ✖ FAIL — 5 blocking findings.
$ npx nobypass check 8 files analyzed · 0 new errors · 0 new warnings · 0 known ✔ PASS — no new blocking architecture violations.
Discover → Review → Approve → Enforce
NoBypass never decides your architecture for you. It proposes, you approve, then it holds every change to that decision.
-
01
Discover
initreadscomposer.json, PSR-4,package.jsonandtsconfigpaths, starts from a preset and writes a proposal. It never treats the most common existing pattern as correct. -
02
Review
discovery showlists the evidence: layers, coverage, ambiguous files and every place existing code contradicts the proposal. -
03
Approve
approveis interactive only. A human signs off on.nobypass.yml; CI and agents can't. -
04
Enforce
check --since origin/mainfails only on violations new to the branch. Legacy debt goes into a reviewed baseline.
The contract is a file you can review
Layers are globs. allowed_direct lists which layer may depend on which — directly. A controller reaching the model through support, service and repository is fine. Importing it is not.
Unknown keys are rejected, exceptions need a written reason, and inline waivers exist only for the warning-level catch rule.
layers:
controller: ["app/Http/Controllers/**/*.php"]
support: ["app/Support/**/*.php"]
service: ["app/Services/**/*.php"]
repository: ["app/Repositories/**/*.php"]
model: ["app/Models/**/*.php"]
dependencies:
allowed_direct:
controller: [support]
support: [service]
service: [repository]
repository: [model]
model: []
rules:
direct_orm_access:
deny_in: [controller, support, service]
allow_in: [repository]
catch_policy:
discourage_in: [service, repository]
severity: warning
Seven rules, all deterministic
Every finding names the file, line, layers, contract edge, approved route and a concrete fix.
| Rule | Catches | Default |
|---|---|---|
BND001 | Forbidden direct dependency between layers | error |
BND002 | Eloquent, DB::, Prisma or TypeORM access outside the approved layers | error |
BND003 | Skipping an intermediate boundary — controller → service when the route is controller → support → service | error |
BND004 | Dependency cycle between layers | error |
EXC001 | catch where the contract discourages it, without a documented purpose | warning |
CFG001 | In-scope file that matches no layer, or several | warning |
ANL001 | File that couldn't be parsed or reference that couldn't be resolved — never a silent pass | error |
Agents get the rules. CI gets the last word.
An instruction file can be ignored. A required check can't. NoBypass does both.
nobypass agents generate --skill
Adds a delimited block to AGENTS.md (asking first if the file exists) and writes a portable SKILL.md. Works with any agent that reads AGENTS.md or Agent Skills.
Before you report a task as finished:
1. Run `npx nobypass check --since origin/main`.
2. If it fails, fix the violations (move code to
the approved layer; don't work around the rule).
Never edit `.nobypass.yml`, the baseline or
`exceptions:` to make the check pass.
GitHub Actions + SARIF
Run it on every pull request, upload SARIF for inline annotations, make it a required check, and protect the contract with CODEOWNERS.
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: npx --yes nobypass@0.1.0 check
--since origin/${{ github.base_ref }}
--format sarif --output nobypass.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: nobypass.sarif }
What it will not claim
NoBypass proves that detectable violations are absent. That is narrower than "your code is correct", on purpose.
- Direct dependencies only. It doesn't prove every request walks through every layer at runtime.
- Untyped instance calls slip through.
$user->save()on a value whose type isn't visible is not detected. - Unknown means unknown. Dynamic calls are reported as unverified, and parse failures fail the check.
- Governance is yours. An agent can edit the contract locally; CODEOWNERS and branch protection are what stop it.
Five minutes to the first check
Node 20 or newer. Nothing else to install.
npx nobypass initWrite a proposal and a discovery report. Source files untouched.npx nobypass discovery showReview layers, coverage and contradictions.npx nobypass approveEdit the proposal if needed, then approve it yourself.npx nobypass agents generate --skillTell your agents about the contract.npx nobypass check --since origin/mainFail only on what's new.