Skip to main content

The enforcement layer for AI coding agents — deterministic, portable constraints (BECs) that block a commit when a rule breaks, for AI-written and human-written code.

Project description

English · Español

becwright

becwright

CI npm PyPI

The enforcement layer for AI coding agents.

Rules that run, not notes that get ignored. Your CLAUDE.md is a sign; becwright is the guard — it runs your rules against the code and blocks the commit when one breaks, no matter which model (or person) wrote it.

Deterministic, not probabilistic · any language · no Python required · blocks the commit and carries the why.

Dogfooded — every commit to this repo is gated by becwright's own .bec/rules.yaml in CI.

Before / after

Your agent writes checkout.py — a hardcoded API key, an eval() on a promo string — and leaves a note to "clean this up later." Nobody does. It ships.

With becwright, the commit never happens:

becwright blocking a commit that hardcodes a secret and calls eval

See it yourself in 5 seconds — no setup, no git, nothing on your machine is touched:

npx becwright demo        # zero-install   ·   or: pipx run becwright demo

Why a guard, not a sign

An AI agent writes a module and notes "this must never log session tokens." Months later another agent regenerates it, never reads the note, and the token lands in the logs. Nobody notices until it blows up in production.

A sign asks; a guard checks. Right before your work is saved, becwright runs your rules against the code: ✅ everything passes → the commit goes through; ❌ a rule is broken → it stops you, names the rule and its why, and waits until you fix it. A CLAUDE.md note is probabilistic — it depends on the agent reading and obeying. A becwright rule is deterministic — it runs against the real code and returns pass/fail, no matter which model made the change:

Note in CLAUDE.md becwright rule
What it does Asks to be respected Verifies it was respected
Depends on The agent reading and obeying Nothing — it runs against the code
Result Likely Guaranteed
Analogy A "speed limit" sign A physical bump in the road

The two layers are complementary: CLAUDE.md prevents (so 95% comes out right the first time), becwright is the safety net for the 5% that slips through.

New to commits and hooks? — the vocabulary in one box

A commit is a saved snapshot of your code in git. A hook is a small script git runs automatically at a set moment — becwright uses the pre-commit hook, which fires just before a commit is saved. You never run it by hand; git does. The rest of this README goes from "just get me started" to the full technical detail — read as far as you need.

Core concept: BEC (Bound Executable Constraint)

A BEC is a constraint with three properties that no current artifact has together:

  • Bound — the rule is born tied to the intent and the decision that created it (the why); it is not a loose rule without context.
  • Executable — it carries a check that runs and returns pass/fail; it is not prose someone promises to respect.
  • Portable — it can be exported from one repo and imported into another, like a package (this is what creates the network effect over time).

Features

  • Deterministic enforcement — a rule is a real check that runs against your code and returns pass/fail, not a note an agent may ignore.
  • Blocks the commit, not just warns — blocking rules stop git commit; warnings inform without blocking.
  • Any language — the engine matches file globs and runs a command; use the no-code forbid regex for Python, JS/TS, Go, Rust, or anything else.
  • Derive rules from your CLAUDE.mdbecwright init --from-claude-md turns the prohibitions it recognizes (secrets, eval, debugger, console.log, breakpoints, a file line cap, …) into enforceable rules; an AI agent can extend that over MCP. Judgment-based guidance stays in CLAUDE.md.
  • Adopt on any codebase--baseline starts rules that already have violations as warnings, so a legacy repo isn't blocked on day one; graduate each to blocking as you clean it.
  • Guaranteed and assisted rules — deterministic rules block with a 100% guarantee; judgment rules (readability, design) live as advisory — they report via your own reviewer check (e.g. an LLM) but never block, and are labelled so you always know which findings are guaranteed vs best-effort.
  • Bound to the why — every rule carries its intent and reason, shown when it fires.
  • Batteries-included checksforbid / require (a pattern that must be present) / max_lines / filename, plus secret, eval, debug and import checks — with per-rule exclude: to silence false positives.
  • Portable BECsexport a rule to a single .bec.yaml and import it into another repo; custom checks travel with their code.
  • Offline catalogbecwright search / add install ready-made rules with no URL, shipped inside the package.
  • No Python required — install via npm/pnpm as a self-contained binary, or via pip/pipx.
  • Fits your setup — native git hook, or plug into the pre-commit framework or Husky.
  • Can't be skipped — a GitHub Action runs becwright on every PR (only the files it changed), so a required check enforces the rules even when the local hook is bypassed with --no-verify.
  • AI-agent ready — Claude Code plugin, check --json, and an MCP server whose tools let an agent propose, preview and add rules from your CLAUDE.md.
  • Tiny & trustworthy — small, dependency-light (pyyaml), no eval/exec, dogfooded in CI.

Use cases

  • Turn your CLAUDE.md into guardrails — the deterministic parts become BECs that can't be ignored; the judgment calls stay as prose.
  • Adopt gradually on a legacy repo--baseline warns on existing debt without blocking commits, then tighten to blocking rule by rule.
  • Stop secrets before they land — API keys, tokens, private keys, hardcoded passwords.
  • Keep debug leftovers outbreakpoint(), pdb, debugger;, console.log, dbg!, stray panic().
  • Ban risky APIs / enforce conventionseval / exec, a file-length cap, file-name rules, or any pattern you forbid with a one-line regex rule.
  • Enforce commit-message rules — Conventional Commits, or block AI attribution trailers, via a target: commit-msg rule and the commit-msg hook.
  • Guard AI-written code — the deterministic net for what an agent regenerates and forgets.
  • Enforce team conventions — encode a decision once as a BEC and share it across every repo.

How to use it

You install becwright once; each project only adds a small .bec/rules.yaml. Two steps and you're done.

1. Install — one line:

npm install -g becwright
Prefer pnpm, pip, or a project-local install? →
pnpm add -g becwright
pipx install becwright                # or: pip install becwright
npm install --save-dev becwright      # project-local; the hook finds it in node_modules/.bin

Via npm/pnpm there's no Python required — a self-contained binary ships per platform (linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64). On any other platform, use pipx install becwright.

2. Set it up — inside your project:

becwright init   # detects your language, writes .bec/rules.yaml, installs the pre-commit hook

That's it. From now on every git commit runs the checks by itself, and stops a commit that breaks a blocking rule. You never call becwright by hand again.

Adopting on an existing codebase? Use becwright init --baseline: rules that already have violations start as warning (nothing legitimate is blocked) while clean rules start as blocking. Fix the debt over time, then graduate each rule to blocking.

Already have a CLAUDE.md? becwright init --from-claude-md reads it and turns the prohibitions it recognizes (secrets, eval, debugger, console.log, breakpoints, …) into enforceable rules — the deterministic safety net under the prose. Judgment-based guidance stays in CLAUDE.md. Review the result; combine with --baseline to adopt on a dirty repo in one step.

Available commands:

Command What it does
becwright demo Show becwright block a sample bad commit (no setup, no git needed)
becwright init Scaffold a starter .bec/rules.yaml and install the hook
becwright init --baseline Same, but start already-violated rules as warning (adopt without blocking)
becwright init --from-claude-md Derive rules from the repo's CLAUDE.md (best-effort)
becwright list List the built-in checks
becwright check Runs the rules over the staged files
becwright check --diff <base> Runs the rules over only the files changed vs <base> (for CI/PR)
becwright why [id] Shows the intent + why behind the rules — the repo's decision memory (--json for agents)
becwright search [query] Lists ready-made BECs from the built-in catalog
becwright add <name> Installs a catalog BEC into .bec/rules.yaml (offline)
becwright install Installs the native pre-commit hook
becwright uninstall Removes the hook
becwright export <id> Exports a BEC to a .bec.yaml file
becwright import <file|URL> Imports a BEC from another repo

Already using pre-commit or Husky?

If your repo already manages git hooks, becwright plugs in without becwright install.

pre-commit — add this to .pre-commit-config.yaml:

repos:
  - repo: https://github.com/DataDave-Dev/becwright
    rev: v0.3.0
    hooks:
      - id: becwright

Husky (JS/TS repos) — in .husky/pre-commit:

npx becwright check

Either way becwright still reads .bec/rules.yaml and blocks the commit on a broken blocking rule. You only need becwright init once to scaffold the rules (skip its hook install if another tool owns the hook).

As a required CI check (GitHub Action)

The commit hook is the first line of defense, but it lives on each developer's machine — and git commit --no-verify skips it. A required CI check cannot be skipped. Running becwright on every pull request turns the rules into infrastructure of the pipeline, not a local convenience that an agent (or a human) can bypass.

Add .github/workflows/becwright.yml:

name: becwright
on: pull_request

jobs:
  becwright:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # full history so the merge-base with the PR base exists
      - uses: DataDave-Dev/becwright@main   # pin to a released tag once available

By default it checks only the files the PR changed against the base branch — pre-existing debt on the rest of the repo never fails the build, so you can adopt it on a large codebase without a red wall. Make the check required in your branch-protection rules and the rules become non-negotiable.

Inputs (all optional):

Input Default What it does
base PR base branch Git ref to diff against; only files changed vs it are checked
version becwright pip specifier to install (e.g. becwright==1.0.0)
python-version 3.x Python used to run becwright
args empty Extra args appended to becwright check

Set fetch-depth: 0 on the checkout so the merge-base with the PR base exists; a shallow clone makes the base ref unreachable and the check fails loudly rather than passing on an empty file list.

Prefer to run it yourself? becwright check --diff origin/main does the same thing from any workflow step, no action needed.

Use with AI agents (Claude Code)

becwright is the deterministic net for what an AI agent lets slip. There is a Claude Code plugin so an agent can install and drive it for you:

/plugin marketplace add DataDave-Dev/becwright
/plugin install becwright@becwright

It adds a becwright skill and a /becwright command. See integrations/claude-code/.

For structured results, becwright check --json prints a machine-readable summary, and becwright mcp (install the mcp extra: pipx install "becwright[mcp]") runs an MCP server — MCP is a standard way for AI tools to plug in extra abilities — exposing check, list_checks and list_rules to any agent. See documentation/mcp.md.

Better yet, an agent can read the rules before it writes code: becwright why --json hands it the decisions it must not violate (each rule's intent and the reason behind it), so it steers clear of a broken commit instead of discovering the rule only when the commit is blocked. The .bec/rules.yaml catalog becomes the repo's queryable decision memory.

Either way the signal stays lean. A blocked commit returns the one rule that broke, its why, and the exact lines — the agent fixes precisely that instead of re-reading the whole style guide into context. The usual advice is "give the model more context"; becwright inverts it — you hand it the specific constraint it broke, checked deterministically, not the entire rulebook. Fewer tokens, tighter loop, and the guarantee doesn't depend on the model having read anything at all.

A rule in .bec/rules.yaml:

rules:
  - id: no-token-in-logs
    intent: >
      Session tokens and credentials must never reach any log.
    why_it_matters: >
      If a token shows up in the logs, anyone with access to them can steal a
      user's session.
    paths: ["src/**/*.py"]
    exclude: ["src/logging_setup.py"]   # optional: globs carved out of paths
    check: "becwright run no_token_in_logs"
    severity: blocking   # blocking = stops the commit | warning = only warns

exclude subtracts globs from paths, so one rule can cover a whole language while skipping the files that would only produce false positives — vendored or generated code, or the check's own implementation. It travels with the rule through export / import. Full field reference: documentation/usage.md.

How becwright compares

becwright is not a linter and not just a hook runner — it is the layer that makes a rule portable and bound to its reason, and blocks the commit on it.

becwright pre-commit / Husky gitleaks / linters CLAUDE.md / .cursorrules
Runs a real check ✅ (runs other tools) ❌ prose
Blocks the commit
Carries the why (intent) ⚠️ not enforced
Portable rule between repos export/import ⚠️ copy config ⚠️ ⚠️
Any language, no per-tool plugin forbid regex ⚠️ ❌ tool-specific n/a

becwright complements these rather than replacing them: run gitleaks or a linter as a becwright check, or add becwright inside pre-commit / Husky. The difference is that a BEC binds the rule to its intent and travels between repos.

Included checks

becwright ships ready-to-use checks. Each one is a module invoked from the check field. They work by searching the text of your files for a pattern (a regex — a search pattern for text, like "find this exact word"), rather than truly understanding the code. That keeps them simple and predictable: they may miss exotic cases, and the real value is in tying each rule to its why.

Check What it detects Language Suggested severity
forbid Any regex you pass (--pattern) any depends on the case
require A regex (--pattern) that must appear (e.g. a license header) any depends on the case
max_lines Files longer than --max lines any warning
filename File names matching --forbid or not matching --require any depends on the case
no_token_in_logs Tokens/credentials in log calls Python blocking
hardcoded_secrets AWS keys, private keys, password = "..." literals any blocking
debug_remnants Forgotten breakpoint(), pdb.set_trace(), import pdb Python blocking
dangerous_eval eval() / exec() calls any blocking
conflict_markers Leftover git merge conflict markers (<<<<<<<) any blocking
wildcard_imports from x import * Python warning

Example rules to copy into your .bec/rules.yaml:

rules:
  - id: no-hardcoded-secrets
    intent: >
      No secret (key, token, password) should be hardcoded in the code.
    why_it_matters: >
      A secret in the repo stays in git history forever and is visible to
      anyone with access to the code.
    paths: ["src/**/*.py"]
    check: "becwright run hardcoded_secrets"
    severity: blocking

  - id: no-debug-remnants
    intent: >
      Debug code (breakpoints, pdb) must not be committed.
    why_it_matters: >
      A forgotten breakpoint hangs the process in production or CI.
    paths: ["src/**/*.py"]
    check: "becwright run debug_remnants"
    severity: blocking

  - id: no-dangerous-eval
    intent: >
      Do not use eval()/exec(), which execute arbitrary code.
    why_it_matters: >
      eval/exec on untrusted input is remote code execution.
    paths: ["src/**/*.py"]
    check: "becwright run dangerous_eval"
    severity: blocking

  - id: no-wildcard-imports
    intent: >
      Avoid 'from x import *', which pollutes the namespace.
    why_it_matters: >
      Wildcard imports hide where each name comes from and break static
      analysis.
    paths: ["src/**/*.py"]
    check: "becwright run wildcard_imports"
    severity: warning

Ready-made rules (no writing required)

Don't want to write rules yourself? The catalog ships inside becwright, so you can install a rule with one command — no URL, works offline. becwright shows you the rule, then drops it into your .bec/rules.yaml, ready to go:

becwright search                 # list every BEC in the catalog
becwright search secret          # filter by a word

becwright add no-token-in-logs   # install one (Python)
becwright add no-debugger-js     # JavaScript / TypeScript
becwright add no-hardcoded-secrets   # any language

The full list (Python, JS/TS, Go, Rust) lives in src/becwright/becs/.

Any language

becwright is language-agnostic: the engine only filters files by their paths (written as globs — file patterns like src/**/*.js, where * means "any name" and ** means "any folder, however deep") and runs the check as a command; it never assumes Python. You can watch JavaScript, Go, Rust, or anything else.

The fastest way to write a rule for another language —without writing code— is the forbid check, which fails if a regex appears in the files:

rules:
  - id: no-debugger-js
    intent: >
      Do not leave 'debugger;' in JavaScript/TypeScript code.
    why_it_matters: >
      A forgotten 'debugger' halts execution and should not reach production.
    paths: ["**/*.js", "**/*.ts"]
    check: "becwright run forbid --pattern '\\bdebugger\\b'"
    severity: blocking

forbid accepts --pattern REGEX, --ignore-case and --message TEXT. For finer checks, write your own script in whatever language you want (an executable that reads the file list from stdin and exits with code 0/1) and point check at it.

Sharing BECs between repos

A BEC is portable: you can take it out of one repo and install it in another. A bundle is a single self-contained .bec.yaml file (the rule + the check's code if it is custom).

# In the source repo: export a rule to a file
becwright export no-token-in-logs -o no-token-in-logs.bec.yaml

# In another repo: import it (from a file or an http/https URL)
becwright import no-token-in-logs.bec.yaml
becwright import https://example.com/no-token-in-logs.bec.yaml

On import, becwright shows the check's code and asks for confirmation before installing it: importing a BEC is importing code that will run on every commit. Use --yes to skip the confirmation in automated environments.

There is a catalog of ready-to-use BECs shipped inside becwright: run becwright search to list them and becwright add <name> to install one (they also live in src/becwright/becs/ for browsing).

Built-in checks (becwright run *) travel with the package, so the bundle only stores their name. A custom check (.bec/checks/foo.py) travels with its code embedded and lands in .bec/checks/ of the target repo.

Documentation

Full docs live in documentation/. Each page opens with a plain-language summary and then goes deeper, so start wherever you are:

  • Just getting started: usage — install, the commands, and how to write a rule.
  • Want to add your own rule: writing checks — from the no-code forbid shortcut to a custom check in any language.
  • Sharing rules between projects: portability.
  • Curious how it works inside: architecture & flow.
  • Wiring it to an AI agent: MCP & JSON output.

Current status

becwright is published and installable on every platform: via npm/pnpm as a self-contained binary (no Python) and via pip/pipx. The packaged engine (src/becwright/) ships a CLI (demo / init / list / check (with --json) / run / install / uninstall / export / import / mcp), a native git hook, built-in checks (Python + the generic forbid for any language), BEC portability between repos, and a catalog with Python, JS/TS, Go and Rust BECs.

For AI agents there is a Claude Code plugin and an MCP server (becwright mcp) alongside structured check --json output. The original prototype is archived under prototype/ as a reference, and the test suite is green.

Future work (AST analysis, deep per-language tooling, cryptographic signing of verifications) is documented in the project plan.

Stability & versioning

becwright is stable (1.0). It's dogfooded (its own commits are gated by becwright), the test suite is green, and it's published on npm and PyPI. Under SemVer the public contract below only breaks on a major bump, so a 1.x upgrade is always safe. If you depend on it in CI, pin a version anyway (becwright==1.0.0, or npm i -g becwright@1.0.0).

The public contract — stable as of 1.0.0, changed only on a major bump:

  • The .bec/rules.yaml schema (rule fields and their meaning).
  • The .bec.yaml bundle format that export / import move between repos.
  • Built-in check names and their flags.
  • CLI commands and their exit codes.
  • The check --json output shape.
  • MCP tool names and signatures.

Everything else (message wording, catalog contents, internal modules) can change at any time.

Before 1.0.0 the groundwork was: both on-disk formats versioned so a newer file fails loudly (schema_version / becwright_bec), the rules.yaml field set frozen and test-locked, exit codes and check --json documented and test-locked, and validation against real repositories.

Deprecation policy — from 1.0.0 on, nothing in the public contract is removed without a major bump of notice. When something has to change:

  1. It is marked deprecated in a minor release — it keeps working and emits a warning.
  2. It keeps working (still warning) through the rest of that major series.
  3. It is removed only in the next major release.

So anything valid on 1.0 stays valid across every 1.x: a breaking change always crosses a major version, with at least one minor of warning first. Pin a version in CI and a 1.x upgrade will never break your rules, bundles, or check scripts without warning.

Roadmap

becwright is intentionally small. On the horizon:

  • Grow the becwright add catalog with more languages and common rules.
  • A landing page and a richer examples/ set.
  • More built-in checks, driven by real usage.

Deliberately out of scope to stay simple and deterministic: AST-based analysis, deep per-language tool suites, and cryptographic signing of BECs.

FAQ

Why not just Ruff / Black / pre-commit? Use them — becwright doesn't compete with them. Black formats, Ruff lints, pre-commit runs tools. None of them hand you a rule bound to its reason that blocks the commit and travels to another repo. becwright is that layer, and it will happily run Ruff or gitleaks as one of its checks. Different job, same pipeline.

It's a young project — why trust it on my commits? Because there's very little to trust: one dependency (pyyaml), no eval/exec, checks that are plain regex you can read in under a minute, and an MIT license. And it's dogfooded — becwright's own commits are gated by becwright. If it broke, this repo wouldn't build.

Can an agent just delete the rule? It can — but deleting a rule is a visible line in the diff that review flags, whereas ignoring a note in CLAUDE.md leaves no trace at all. A guard you have to remove on camera beats a sign you can walk past.

Doesn't pre-commit already do this? It runs tools; it doesn't give you a rule that carries its why and travels between repos. You can even run becwright inside pre-commit — see above.

Do I need Python? No. npm i -g becwright installs a self-contained binary; pipx install becwright also works.

Does it work on Windows? Yes, via Git Bash (the git hook is a sh script, which Git for Windows provides). The becwright CLI itself is cross-platform.

How do I ignore a single line? Add a becwright: ignore comment on it.

How is "becwright" pronounced / what does it mean? bec-wright — a "wright" is a maker (as in playwright), so becwright is "the one who makes BECs".

Is it safe to import a BEC? becwright shows the check's code and asks for confirmation before installing. Treat an untrusted bundle like any untrusted script.

Contributing

Contributions are welcome — see CONTRIBUTING.md and the Code of Conduct. Found a security issue? Please follow the security policy. The changelog tracks every release.

License

MIT © Alonso David De Leon Rodarte

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

becwright-1.0.0.tar.gz (68.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

becwright-1.0.0-py3-none-any.whl (53.0 kB view details)

Uploaded Python 3

File details

Details for the file becwright-1.0.0.tar.gz.

File metadata

  • Download URL: becwright-1.0.0.tar.gz
  • Upload date:
  • Size: 68.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for becwright-1.0.0.tar.gz
Algorithm Hash digest
SHA256 41b7b320a03bcc526c02832b2f9d34275f0f0fe6a3bc735f6398295cbb54f07e
MD5 6dab72465379a485f99ca8f10cb60871
BLAKE2b-256 6bb66be49589267626ed2c23992e600719354fceb7b42df49ab5ff18a9fbbd54

See more details on using hashes here.

Provenance

The following attestation bundles were made for becwright-1.0.0.tar.gz:

Publisher: release.yml on DataDave-Dev/becwright

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file becwright-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: becwright-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 53.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for becwright-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c2604c09c342db24a2b389a97a70bf20fd2e2c3ec69c436187e79f9bee57e13c
MD5 ce8256e718118da1c7e3606e73aced3d
BLAKE2b-256 ffedc70da535b7700c05ac7836a9fdff2cc97144c7a4b146aa740246dac4258d

See more details on using hashes here.

Provenance

The following attestation bundles were made for becwright-1.0.0-py3-none-any.whl:

Publisher: release.yml on DataDave-Dev/becwright

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page