Deterministic bad-pattern detection for codebases.
Badbox lets a repository encode questionable engineering patterns as structural rules, then check for them without an LLM. It reports evidence—where a pattern occurred, who owns it, and how often— while leaving the decision to a developer or coding agent.
Badbox ships no default policies and performs no automatic rewriting. Each project owns its rules
under .badbox/.
Current npm version: 0.3.0
Current rule format: #badbox 1 (experimental)
Badbox requires Bun 1.3.14 or newer.
Install it in a project:
bun add --dev badbox
Then run it through bunx:
bunx badbox check
You can also install the CLI globally:
bun add --global badbox
badbox check
Published packages include prebuilt native engines for these platforms:
| Platform | Architecture | Native package |
|---|---|---|
| macOS | Apple Silicon | badbox-darwin-arm64 |
| macOS | Intel | badbox-darwin-x64 |
| Linux GNU | arm64 | badbox-linux-arm64-gnu |
| Linux GNU | x64 | badbox-linux-x64-gnu |
| Windows | x64 | badbox-windows-x64 |
Installing from npm on a supported platform does not require a local Rust toolchain.
Run create from the root of the project you want to check:
bunx badbox create project-rules.badbox
Badbox creates .badbox/project-rules.badbox with the correct version header and an editable
example. Replace that example with a rule for your project:
#badbox 1
rule rust/excessive-clones for rust {
summary "Function contains more clone calls than the configured limit"
param limit = 4
find code(value) `value.clone()`
group by nearest callable
when count > limit
report {
severity warning
message "Function contains excessive clone calls"
evidence "clone call sites"
}
}
Check the project:
bunx badbox check
Or check specific source roots while still loading rules from the project's .badbox/ directory:
bunx badbox check src packages
A finding is an observation, so findings do not make the command fail. Invalid rules, invalid input, unreadable files, I/O errors, and syntax diagnostics make the check incomplete and return a nonzero exit code.
A rule selects syntax, assigns each match to an owner, applies structural conditions, counts the remaining matches, and reports owners above a threshold.
find -> where -> nearest owner -> distinct-range count -> threshold -> finding
This makes rules useful for patterns such as:
Badbox detects which supported languages occur under the requested source roots and runs only the relevant rules.
The tiny DSL is the primary rule format. Every file starts with a version header:
#badbox 1
Names declared in code(...) become structural captures:
find code(value) `value.clone()`
Undeclared identifiers remain literal source syntax. Captures can also be constrained by text:
where text(method) in ["unwrap", "expect"]
Supported text operators are ==, !=, in, not in, and matches.
Rules can inspect containment and evidence around a selected match:
where match inside any {
node for_statement
node while_statement
}
where group lacks any {
code(ctx) `ctx.cancel()`
}
Ordering relations compare statements in the same nearest callable and lexical block:
find code(statement) `statement.clearBindings()`
group by nearest callable
where match follows any {
code(statement) `statement.reset()`
}
Repeating statement in both selectors requires the captured source text to be equal. Therefore,
first.reset() does not satisfy a later second.clearBindings() match. Intervening sibling
statements are allowed; nested callables and different branch, loop, switch, or error-handling
blocks remain separate.
See the tiny DSL reference for the complete grammar, validation rules,
relations, parameters, and fixture declarations. Runnable DSL examples live under
examples/rules. YAML equivalents under examples/yaml exercise
the compatibility frontend; they are never loaded automatically.
| Area | Supported today |
|---|---|
| Selection | Source-shaped code(...) patterns and raw syntax-node kinds |
| Ownership | Nearest callable for Rust, Go, PowerShell, and Zig; explicit nearest node kinds elsewhere |
| Capture predicates | ==, !=, in, not in, and Rust-regex matches |
| Containment | where match inside any|all |
| Owner evidence | where group has any|all and where group lacks any|all |
| Ordering | Statement-level follows and precedes within one callable and lexical block |
| Aggregation | Distinct selected ranges counted per owner with strict count > threshold |
| Parameters | Rule-local scalar defaults with programmatic overrides |
| Frontends | Tiny DSL plus YAML compatibility, both compiled to the same Badbox Rule IR |
| Output | Deterministically ordered, bounded findings with exact total counts |
Badbox bundles 30 parsers.
The 28 ast-grep built-in languages are Bash, C, C++, C#, CSS, Dart, Elixir, Go, Haskell, HCL, HTML, Java, JavaScript/JSX, JSON, Kotlin, Lua, Markdown, Nix, PHP, Python, Ruby, Rust, Scala, Solidity, Swift, TSX, TypeScript, and YAML.
Badbox also statically links PowerShell and Zig parsers.
File extensions select relevant language rules. This is language detection, not framework, dependency, build-configuration, or semantic type detection.
badbox create <name.badbox>
badbox check [path ...]
createCreates .badbox/<name.badbox> with the current version header and a Rust example. The name may
include subdirectories under .badbox/. Badbox refuses absolute paths, parent traversal, names
without the .badbox suffix, and existing files.
checkRecursively loads .badbox, .yaml, and .yml rule files from the current project's .badbox/
directory. With no source paths, it checks the current project. Explicit source paths narrow source
discovery but do not change where rules are loaded from.
Discovery respects ignore files and hidden paths, skips common build and vendor directories, and does not follow nested symlinks.
Use badbox/checker when another tool or coding agent needs structured results:
import { inspect, iterateFindings } from "badbox/checker";
const result = await inspect({
paths: ["./src"],
rulePaths: ["./.badbox"],
parameters: {
"rust/excessive-clones.limit": 6,
},
maxFindings: 1_000,
});
for (const finding of iterateFindings(result)) {
console.log({
rule: finding.rule.id,
file: finding.file,
owner: [finding.ownerStart, finding.ownerEnd],
observed: finding.observed,
});
}
if (result.diagnostics.length > 0) {
console.error(result.diagnostics);
}
inspect() also accepts:
threshold to replace every loaded rule's threshold;profile: true to include phase timings and cache/execution counters;maxFindings to bound returned records while retaining the exact findingCount.Findings are stored as five u32 values: file ID, rule ID, owner start, owner end, and observed
count. File paths and rule metadata are interned separately. Use iterateFindings() to decode the
records without materializing another result array.
flowchart TD
CLI["CLI or TypeScript inspect()"]
Native["Native N-API boundary"]
Frontend["DSL or YAML frontend"]
IR["Badbox Rule IR"]
Plan["Per-language shared finder plan"]
Parser["Tree-sitter parsing and bounded caches"]
Match["ast-grep structural backend"]
Eval["Ownership, relations, counting, thresholds"]
Result["Compact findings and metadata"]
CLI --> Native --> Frontend --> IR --> Plan --> Parser --> Match --> Eval --> Result
Parsing, matching, ownership, relational evaluation, counting, thresholds, and compact result construction run in Rust. TypeScript invokes the native engine and decodes metadata.
Badbox owns the Rule IR and structural-backend interface; ast-grep is an implementation detail. Identical primary and relational selectors are interned across a language plan and dispatched once per relevant AST node. Rule-specific predicates, ownership, thresholds, and reporting remain independent.
File work uses at most four native workers. Compiled-rule, parsed-file, and compact-result caches are content-validated, process-local, and bounded. Parallel results are sorted before returning so output remains deterministic.
follows and precedes allow intervening sibling
statements.maxFindings adds 20 bytes per
returned finding.1 remains experimental.Building from source requires Bun, Rust/Cargo, cargo-nextest, and a C compiler for Tree-sitter
grammars.
bun install
bun run build:native
bun run ci
bun run ci is the canonical full check. It runs Rust formatting, Clippy, both cargo-nextest
suites, the native release build, TypeScript checking, and all Bun tests.
After changing Rust, rebuild the .node addon before running Bun tests:
bun run build:native
bun test tests/scanner.test.ts
For reproducible latency, rule-scaling, and memory measurements:
bun run build:native
bun run benchmark
bun run benchmark ../zova
See the benchmark methodology, optimization report, and frozen Zova corpus.
| Path | Purpose |
|---|---|
src/cli.ts | check and create commands |
src/scanner/ | Public TypeScript API and native-addon loading |
native/src/rule_ir.rs | Backend-independent rule model |
native/src/frontends/ | DSL/YAML lowering and validation |
native/src/backend/ | Structural backend contract and ast-grep implementation |
native/src/evaluator.rs | Language-independent aggregation and findings |
native/tiny-dsl/ | Rust parser crate, tests, and DSL reference |
examples/ | Runnable rules that are never defaults |
tests/ | CLI, integration, packaging, corpus, and benchmark-contract tests |
benchmarks/ | Reproducible performance harness and reports |
All six npm packages and both Rust manifests must use the same release version. Validate that invariant before publishing:
bun run scripts/validate-release.ts 0.3.0
bun run ci
After committing and pushing the version, run the Release npm packages GitHub Actions workflow
with that version. It builds and tests all five native targets, packages and smoke-tests the npm
tarballs, publishes the platform packages first, and publishes badbox last.
All six npm packages use npm trusted publishing through GitHub OIDC; no NPM_TOKEN is required.
Stable versions publish under latest, while prerelease versions publish under next.
MIT © 2026 Octacity
20 commits
Rust
59.8%
TypeScript
40.2%
Deterministic bad-pattern detection for codebases.
Badbox lets a repository encode questionable engineering patterns as structural rules, then check for them without an LLM. It reports evidence—where a pattern occurred, who owns it, and how often— while leaving the decision to a developer or coding agent.
Badbox ships no default policies and performs no automatic rewriting. Each project owns its rules
under .badbox/.
Current npm version: 0.3.0
Current rule format: #badbox 1 (experimental)
Badbox requires Bun 1.3.14 or newer.
Install it in a project:
bun add --dev badbox
Then run it through bunx:
bunx badbox check
You can also install the CLI globally:
bun add --global badbox
badbox check
Published packages include prebuilt native engines for these platforms:
| Platform | Architecture | Native package |
|---|---|---|
| macOS | Apple Silicon | badbox-darwin-arm64 |
| macOS | Intel | badbox-darwin-x64 |
| Linux GNU | arm64 | badbox-linux-arm64-gnu |
| Linux GNU | x64 | badbox-linux-x64-gnu |
| Windows | x64 | badbox-windows-x64 |
Installing from npm on a supported platform does not require a local Rust toolchain.
Run create from the root of the project you want to check:
bunx badbox create project-rules.badbox
Badbox creates .badbox/project-rules.badbox with the correct version header and an editable
example. Replace that example with a rule for your project:
#badbox 1
rule rust/excessive-clones for rust {
summary "Function contains more clone calls than the configured limit"
param limit = 4
find code(value) `value.clone()`
group by nearest callable
when count > limit
report {
severity warning
message "Function contains excessive clone calls"
evidence "clone call sites"
}
}
Check the project:
bunx badbox check
Or check specific source roots while still loading rules from the project's .badbox/ directory:
bunx badbox check src packages
A finding is an observation, so findings do not make the command fail. Invalid rules, invalid input, unreadable files, I/O errors, and syntax diagnostics make the check incomplete and return a nonzero exit code.
A rule selects syntax, assigns each match to an owner, applies structural conditions, counts the remaining matches, and reports owners above a threshold.
find -> where -> nearest owner -> distinct-range count -> threshold -> finding
This makes rules useful for patterns such as:
Badbox detects which supported languages occur under the requested source roots and runs only the relevant rules.
The tiny DSL is the primary rule format. Every file starts with a version header:
#badbox 1
Names declared in code(...) become structural captures:
find code(value) `value.clone()`
Undeclared identifiers remain literal source syntax. Captures can also be constrained by text:
where text(method) in ["unwrap", "expect"]
Supported text operators are ==, !=, in, not in, and matches.
Rules can inspect containment and evidence around a selected match:
where match inside any {
node for_statement
node while_statement
}
where group lacks any {
code(ctx) `ctx.cancel()`
}
Ordering relations compare statements in the same nearest callable and lexical block:
find code(statement) `statement.clearBindings()`
group by nearest callable
where match follows any {
code(statement) `statement.reset()`
}
Repeating statement in both selectors requires the captured source text to be equal. Therefore,
first.reset() does not satisfy a later second.clearBindings() match. Intervening sibling
statements are allowed; nested callables and different branch, loop, switch, or error-handling
blocks remain separate.
See the tiny DSL reference for the complete grammar, validation rules,
relations, parameters, and fixture declarations. Runnable DSL examples live under
examples/rules. YAML equivalents under examples/yaml exercise
the compatibility frontend; they are never loaded automatically.
| Area | Supported today |
|---|---|
| Selection | Source-shaped code(...) patterns and raw syntax-node kinds |
| Ownership | Nearest callable for Rust, Go, PowerShell, and Zig; explicit nearest node kinds elsewhere |
| Capture predicates | ==, !=, in, not in, and Rust-regex matches |
| Containment | where match inside any|all |
| Owner evidence | where group has any|all and where group lacks any|all |
| Ordering | Statement-level follows and precedes within one callable and lexical block |
| Aggregation | Distinct selected ranges counted per owner with strict count > threshold |
| Parameters | Rule-local scalar defaults with programmatic overrides |
| Frontends | Tiny DSL plus YAML compatibility, both compiled to the same Badbox Rule IR |
| Output | Deterministically ordered, bounded findings with exact total counts |
Badbox bundles 30 parsers.
The 28 ast-grep built-in languages are Bash, C, C++, C#, CSS, Dart, Elixir, Go, Haskell, HCL, HTML, Java, JavaScript/JSX, JSON, Kotlin, Lua, Markdown, Nix, PHP, Python, Ruby, Rust, Scala, Solidity, Swift, TSX, TypeScript, and YAML.
Badbox also statically links PowerShell and Zig parsers.
File extensions select relevant language rules. This is language detection, not framework, dependency, build-configuration, or semantic type detection.
badbox create <name.badbox>
badbox check [path ...]
createCreates .badbox/<name.badbox> with the current version header and a Rust example. The name may
include subdirectories under .badbox/. Badbox refuses absolute paths, parent traversal, names
without the .badbox suffix, and existing files.
checkRecursively loads .badbox, .yaml, and .yml rule files from the current project's .badbox/
directory. With no source paths, it checks the current project. Explicit source paths narrow source
discovery but do not change where rules are loaded from.
Discovery respects ignore files and hidden paths, skips common build and vendor directories, and does not follow nested symlinks.
Use badbox/checker when another tool or coding agent needs structured results:
import { inspect, iterateFindings } from "badbox/checker";
const result = await inspect({
paths: ["./src"],
rulePaths: ["./.badbox"],
parameters: {
"rust/excessive-clones.limit": 6,
},
maxFindings: 1_000,
});
for (const finding of iterateFindings(result)) {
console.log({
rule: finding.rule.id,
file: finding.file,
owner: [finding.ownerStart, finding.ownerEnd],
observed: finding.observed,
});
}
if (result.diagnostics.length > 0) {
console.error(result.diagnostics);
}
inspect() also accepts:
threshold to replace every loaded rule's threshold;profile: true to include phase timings and cache/execution counters;maxFindings to bound returned records while retaining the exact findingCount.Findings are stored as five u32 values: file ID, rule ID, owner start, owner end, and observed
count. File paths and rule metadata are interned separately. Use iterateFindings() to decode the
records without materializing another result array.
flowchart TD
CLI["CLI or TypeScript inspect()"]
Native["Native N-API boundary"]
Frontend["DSL or YAML frontend"]
IR["Badbox Rule IR"]
Plan["Per-language shared finder plan"]
Parser["Tree-sitter parsing and bounded caches"]
Match["ast-grep structural backend"]
Eval["Ownership, relations, counting, thresholds"]
Result["Compact findings and metadata"]
CLI --> Native --> Frontend --> IR --> Plan --> Parser --> Match --> Eval --> Result
Parsing, matching, ownership, relational evaluation, counting, thresholds, and compact result construction run in Rust. TypeScript invokes the native engine and decodes metadata.
Badbox owns the Rule IR and structural-backend interface; ast-grep is an implementation detail. Identical primary and relational selectors are interned across a language plan and dispatched once per relevant AST node. Rule-specific predicates, ownership, thresholds, and reporting remain independent.
File work uses at most four native workers. Compiled-rule, parsed-file, and compact-result caches are content-validated, process-local, and bounded. Parallel results are sorted before returning so output remains deterministic.
follows and precedes allow intervening sibling
statements.maxFindings adds 20 bytes per
returned finding.1 remains experimental.Building from source requires Bun, Rust/Cargo, cargo-nextest, and a C compiler for Tree-sitter
grammars.
bun install
bun run build:native
bun run ci
bun run ci is the canonical full check. It runs Rust formatting, Clippy, both cargo-nextest
suites, the native release build, TypeScript checking, and all Bun tests.
After changing Rust, rebuild the .node addon before running Bun tests:
bun run build:native
bun test tests/scanner.test.ts
For reproducible latency, rule-scaling, and memory measurements:
bun run build:native
bun run benchmark
bun run benchmark ../zova
See the benchmark methodology, optimization report, and frozen Zova corpus.
| Path | Purpose |
|---|---|
src/cli.ts | check and create commands |
src/scanner/ | Public TypeScript API and native-addon loading |
native/src/rule_ir.rs | Backend-independent rule model |
native/src/frontends/ | DSL/YAML lowering and validation |
native/src/backend/ | Structural backend contract and ast-grep implementation |
native/src/evaluator.rs | Language-independent aggregation and findings |
native/tiny-dsl/ | Rust parser crate, tests, and DSL reference |
examples/ | Runnable rules that are never defaults |
tests/ | CLI, integration, packaging, corpus, and benchmark-contract tests |
benchmarks/ | Reproducible performance harness and reports |
All six npm packages and both Rust manifests must use the same release version. Validate that invariant before publishing:
bun run scripts/validate-release.ts 0.3.0
bun run ci
After committing and pushing the version, run the Release npm packages GitHub Actions workflow
with that version. It builds and tests all five native targets, packages and smoke-tests the npm
tarballs, publishes the platform packages first, and publishes badbox last.
All six npm packages use npm trusted publishing through GitHub OIDC; no NPM_TOKEN is required.
Stable versions publish under latest, while prerelease versions publish under next.
MIT © 2026 Octacity
20 commits
Rust
59.8%
TypeScript
40.2%