Skip to content
项目

Dicklesworthstone/ultimate_bug_scanner has 316 GitHub stars and 44 forks. Star changes are not measured yet. Last checked 2026-11-11 12:07 UTC. Last push 2026-11-11.

这些数字如何统计

Static analysis tool that catches 1000+ bug patterns across all popular programming languages, with auto-wiring into AI coding agent quality guardrails

实测星标增长

24小时
更新延迟
7天
更新延迟
15天
更新延迟
1个月
更新延迟

各周期内的星标净变化,周期相互重叠,并非每日新增。缺失的测量保持空白。星标代表关注度,不代表软件质量。

README.md

原文

🔬 Ultimate Bug Scanner

Ultimate Bug Scanner - The AI Coding Agent's Secret Weapon

The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On

License: MIT Platform Version

# One command to catch 1000+ bug patterns (always main, cache-busted)
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/install.sh?$(date +%s)" \
  | bash -s --

Or via Homebrew (macOS/Linux):

brew install dicklesworthstone/tap/ubs

Just want it to do everything without confirmations? Live life on the edge with easy-mode to auto-install every dependency, accept all prompts, detect local coding agents, and wire their quality guardrails with zero extra questions:

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/install.sh?$(date +%s)" \
  | bash -s -- --easy-mode

Note: Windows users must run the installer one-liner from within Git Bash, or use WSL for Windows.


🤖 Agent Quickstart (JSON/TOON)

Use machine-readable output in agent contexts. stdout = data, stderr = diagnostics, exit 0 = success.

# Scan current repo (JSON)
ubs . --format=json

# Token-optimized output (TOON; needs toon_rust's encoder — install.sh or `ubs doctor --fix` fetches it; without it ubs exits 2 with the error envelope)
ubs . --format=toon

# Scan only staged changes
ubs --staged --format=json

# CI-strict (fail on warnings)
ubs . --profile=strict --fail-on-warning --format=json

# Machine-readable docs and schemas for agent loops
ubs robot-docs                # guide, commands/flags, examples, exit codes, formats, env — one JSON document
ubs robot-docs exit-codes     # a single topic
ubs explain py.security.open-redirect  # rule explanation: remediation, fixture examples, confidence
ubs --schema=json             # JSON Schema (draft 2020-12) for --format=json; also jsonl|sarif|toon|error|all

Every machine format prints exactly one document (or one JSONL stream) on stdout and validates against ubs --schema; status is ok (every requested scanner completed), partial (the scan is incomplete but something usable came back) or error (a module could not run at all and no other scanner produced a result), and pre-scan refusals are an error envelope with exit 2 (see "Machine-readable failures" below). The status field, the human status line and the exit code always agree: anything other than ok exits 2. UBS_ALLOW_PARTIAL=1 makes a partial run exit on the findings of the scanners that completed instead (0 clean, 1 findings). A run in which no scanner completed (an error run, or one whose only requested modules timed out or crashed) has none and still exits 2.

Checkouts also expose the optional local service through ubs serve --repo ., ubs --client --repo . src/main.py, and ubs daemon status|stop|cancel --repo .. The client falls back to an ordinary scan when no matching service is running. Installers and self-update carry its checksum-pinned companion; portable bundles include it too. A warmed engine for edited files remains unfinished. See the service guide.

💥 The Problem: AI Moves Fast, Bugs Move Faster

You're coding faster than ever with Claude Code, Codex, Cursor, and other AI coding agents. You're shipping features in minutes that used to take days. But here's the painful truth:

Even the best AI makes these mistakes:

JavaScript/TypeScript example (similar patterns exist in Python, Go, Rust, Java, C++, Ruby):

// ❌ CRITICAL BUG #1: Null pointer crash waiting to happen
const submitButton = document.getElementById('submit');
submitButton.addEventListener('click', handleSubmit);  // 💥 Crashes if element doesn't exist

// ❌ CRITICAL BUG #2: XSS vulnerability
function displayUserComment(comment) {
  document.getElementById('comments').innerHTML = comment;  // 🚨 Security hole
}

// ❌ CRITICAL BUG #3: Silent failure (missing await)
async function saveUser(data) {
  const result = validateUser(data);  // 💥 Should be 'await validateUser(data)'
  await saveToDatabase(result);  // Saves undefined!
}

// ❌ CRITICAL BUG #4: Always false comparison
if (calculatedValue === NaN) {  // 💥 This NEVER works (always false)
  console.log("Invalid calculation");
}

// ❌ CRITICAL BUG #5: parseInt footgun
const zipCode = parseInt(userInput);  // 💥 "08" becomes 0 in old browsers (octal!)

Each of these bugs could cost 3-6 hours to debug in production. Similar issues plague every language: unguarded null access, missing await, security holes from eval(), buffer overflows from strcpy(), .unwrap() panics, goroutine leaks... You've probably hit all of them.


🎯 The Solution: Your 24/7 Bug Hunting Partner

🧠 Language-Aware Meta-Runner

  • ubs auto-detects JavaScript/TypeScript, Python, C/C++, Rust, Go, Java, Kotlin, Ruby, Swift, C#, Elixir, Bash, PHP, PostgreSQL SQL, and Terraform HCL in the same repo and fans out to per-language scanners.
  • Each scanner lives under modules/ubs-<lang>.sh, ships independently, and supports --format text|json|jsonl|sarif|toon for consistent downstream tooling.
  • Installed releases use a complete verified module generation under ${XDG_DATA_HOME:-$HOME/.local/share}/ubs/modules. Source checkouts and legacy installations retain lazy module resolution (PATH → repo modules/ → cache).
  • Results from every language merge into one text/JSON/SARIF report via jq, so CI systems and AI agents only have to parse a single artifact.

🔐 Supply-Chain Safeguards

  • Every lazily-downloaded module (and its helper assets) ships with pinned SHA-256 checksums baked into the meta-runner. Files fetched from GitHub are verified before they can execute, preventing tampering between releases.
  • The cache lives under ${XDG_DATA_HOME:-$HOME/.local/share}/ubs/modules by default; use --module-dir to relocate it (e.g., inside a CI workspace) while retaining the same verification guarantees.
  • Run ubs doctor at any time to audit your environment. It checks for curl/wget availability, writable cache directories, and per-language module integrity. Add --fix to redownload missing or corrupted modules proactively.
  • Scanner runs still respect --update-modules, but an invalid checksum now causes an immediate failure with remediation guidance rather than executing unverified code.
  • Developer Pre-commit Hook: The repository ships with a .githooks/pre-commit hook that auto-updates SHA256SUMS when modules change and blocks commits with stale checksums. This ensures every release has verified checksums without manual intervention.
  • Authenticated Updates: Install minisign to authenticate release manifests with the embedded public key. UBS_MINISIGN_PUBKEY selects an explicitly trusted alternate key; signatures remain mandatory for ubs --update, including unattended cron jobs.

🎛 Category Packs & Shareable Reports

  • --category=resource-lifecycle focuses the scanners on Python/Go/Java resource hygiene (context managers, defer symmetry, try-with-resources). UBS automatically narrows the language set to those with lifecycle packs enabled and suppresses unrelated categories.
  • --comparison=<baseline.json> diff the latest combined summary against a stored run. Deltas feed into console output, JSON, HTML, and SARIF automation metadata so CI can detect regressions.
  • --report-json=<file> writes an enriched summary (project, totals, git metadata, optional comparison block) that you can archive or share with teammates/CI.
  • --python-pattern-counts=<file> writes unthresholded per-file/per-rule Python pattern counts, with threshold, severity, context and completeness metadata. See the count contract; this is independent of capped finding samples.
  • --all-occurrences opts JSONL into individual source occurrences with code, stable identities and explicit completeness. --max-occurrences=N bounds the export without changing scan counts. See complete occurrence export.
  • --html-report=<file> emits a standalone HTML preview showing totals, trends vs. baseline, and per-language breakdowns—ideal for attaching to PRs or chat updates.
  • --also=FORMAT:FILE writes another rendering of the same scan, including the full findings ledger. Repeat it for JSON, JSONL, SARIF, or TOON; each selected module runs once. The primary --format still controls stdout. Relative destinations use the invocation directory, and two reports cannot target the same file. TOON requires the same verified encoder as --format=toon.
  • All shareable outputs inject GitHub permalinks when UBS is run inside a git repo with a GitHub remote. Text output annotates path:line references, stdout JSON gains git.* metadata plus a permalink on every finding sample, and merged SARIF runs carry properties.permalink on each result location together with versionControlProvenance and automationDetails keyed by the comparison id. Permalinks are relative to the repository root even when you scan a subdirectory.

Resource lifecycle heuristics in each language

  • Python – Category 16 now correlates every open() call against matching with open(...) usage and explicit encoding= parameters, while Category 19 uses the new AST helper at modules/helpers/resource_lifecycle_py.py to walk every file, socket, subprocess, asyncio task, and context cancellation path. The helper resolves alias imports, context managers, and awaited tasks so the diff counts (acquire=X, release=Y, context-managed=Z) show the exact imbalance per file.
  • Go – Category 5/17 now run a Go AST walker (modules/helpers/resource_lifecycle_go.go) that detects context.With* calls missing cancel, time.NewTicker/NewTimer without Stop, os.Open/sql.Open without Close, and mutex Lock/Unlock symmetry. Category 9 also tracks request query/header/form/framework values into response headers unless they strip or reject CR/LF, tracks redirect targets into http.Redirect, framework Redirect calls, and Location headers unless they pass through same-origin or explicit allow-list validation, flags request-derived reverse proxy targets flowing into httputil.NewSingleHostReverseProxy, ProxyRequest.SetURL, or Director URL mutation without HTTPS plus host allow-list validation, flags request-derived SQL text flowing into ExecContext/QueryContext, sqlx-style helpers, or query-builder predicates unless request data is passed as bound parameters, flags credentialed wildcard or reflected-origin CORS responses, and catches auth/session cookies missing HttpOnly, Secure, or SameSite protections. Findings come straight from the AST/resource helper or taint pass positions, so “ticker missing Stop()” and header/open-redirect/reverse-proxy/CORS/cookie/SQL lines map to exact file:line references instead of coarse regex summaries.
  • Java / Kotlin – Category 5 surfaces FileInputStream, readers/writers, JDBC handles, etc. that were created outside try-with-resources, while Category 19 keeps tracking executor services and file streams that never close. Category 4 also tracks servlet/Spring/Ktor request parameters, headers, and annotated parameters into response headers unless they strip/reject CR/LF or encode header fragments, and into redirect sinks such as sendRedirect, respondRedirect, Spring redirect: views, RedirectView, ModelAndView, and Location headers unless a same-origin or explicit allow-list helper is applied first. The summary text matches the manifest fixtures, so CI will fail if regression swallows these warnings.

Java and Kotlin request identity

The redirect, path traversal, response-header and outbound-URL analyzers recognize selected jakarta.servlet and javax.servlet request APIs by their declared types. A ServletRequest or HttpServletRequest can have an arbitrary variable name; selected aliases and same-file helper returns carry its request identity. The supported sources include getParameter and the HTTP request getters getHeader, getQueryString, getRequestURI, getPathInfo and getServletPath.

An unrelated local type named HttpServletRequest, or a non-Servlet object named request, does not establish Servlet provenance. Unsupported overloads retain ordinary argument-flow analysis, and unknown external transformations do not inherit a request type merely from their input. These checks extend the shared bounded request-flow engine without resolving arbitrary external Servlet implementations. Existing public rule IDs and sink locations are unchanged.

The same four analyzers recognize selected Kotlin Ktor member properties by their actual API receiver type:

Proven receiver Supported request members
ApplicationCall, PipelineCall, RoutingCall .request carries request-object identity; .parameters supplies request input
ApplicationRequest, PipelineRequest, RoutingRequest .queryParameters, .rawQueryParameters, .headers supply request input
RoutingCall specifically .pathParameters, .queryParameters also supply request input

Import aliases, selected type aliases, local aliases and supported same-file helper arguments/returns preserve this identity. Extracted collections carry request input through indexing and .get(). A bare Parameters or Headers value, an ordinary map, or a proven local lookalike does not become request input from its type or member names alone. Calls named request(), headers() or queryParameters() remain distinct from the API's member properties.

This bounded Ktor adapter does not infer implicit route-DSL receivers or resolve extension-only request APIs such as uri, path(), header() and receive*. Request-derived field initialization that requires unmodeled object state reports incomplete analysis. Unknown calls preserve existing input taint while dropping unsupported request-object identity; the separate JDBC analysis is unchanged.

Kotlin JDBC SQL extensions

Kotlin category 13 follows selected Servlet and JDK request values into JDBC execution. Same-file extension functions on supported request and JDBC types carry their actual receiver through the existing function summaries. Explicit this and unqualified selected getter or SQL calls use that receiver; a local getter overload keeps the provenance of its actual fallback argument.

Extension dispatch follows the declared receiver type, including supported import and type aliases. Compatible API members keep precedence over a same-named extension. A prepared-statement receiver retains its captured SQL template across helper calls: binding a request value to a static placeholder stays clean, while binding a value cannot sanitize an already tainted template.

This bounded adapter supports nonnullable top-level extensions on receivers whose declared type can be proved. Nullable or generic receivers, nested and member extensions, grouped or chained receiver calls, callback scopes and unresolved mutable receiver effects remain explicitly incomplete.

Kotlin resource analysis and custom catch handlers

Resource-free Kotlin code can continue through a supported catch handler whose exception hierarchy is unresolved. The analyzer retains both caught and uncaught outcomes, including assignments, returns, rethrows and finally; it does not infer that one exception type inherits from another.

The unresolved relation stays attached to those paths. If a path subsequently acquires or observes a selected resource, including through a local helper or captured handle, analysis remains explicitly incomplete. Owned, borrowed and previously closed handles still require the hierarchy proof. Independent functions keep their own ownership state, so an unrelated cancellation handler does not prevent their resource findings from being analyzed.

Elixir raw SQL request flow

Elixir category 4 tracks request-derived query text through local helpers, aliases, immutable rebinding, branch joins and iodata into Ecto.Adapters.SQL.query, query!, query_many and query_many!. Selected imports and same-file repositories with an explicit Ecto.Repo declaration and a Postgres, MyXQL or Tds adapter establish the supported SQL receivers. A separate parameter list does not sanitize SQL text that already contains request data; fixed SQL with bound values remains clean.

The existing ex.security.sql-interpolation rule identifies the execution site in JSON and SARIF. Request sources follow the existing Plug/Phoenix conventions: connection members, request-map patterns and params/query_params bindings. This bounded analysis does not expand arbitrary controller macros, discover external repository declarations or model lazy SQL stream consumption. Unresolved tainted calls and exhausted analysis budgets produce an explicit incomplete result while preserving findings already established. The separate legacy fragment heuristic remains; it is not evidence of complete Ecto query-macro support.

Selected anonymous functions participate in the same request-flow analysis. Direct callback.(...) invocation and synchronous Enum.each/Enum.map over finite lists carry arguments, captured values and local helper effects into the actual sink. A callback retains the value captured when it was created; later rebinding does not change that capture. Creating an unused callback does not execute its body. The shared callback semantics also let the resource analyzer recognize a handle closed through a stored local callback.

Selected zero-argument workers passed to Task.async, spawn, Kernel.spawn and :erlang.spawn retain their immutable captures and expose request-derived sinks in the worker body. Task.await follows the result of its exact task, including aliases, helper returns and tuple members. Worker-only validation or rebinding cannot certify the caller's values; a failed spawned worker does not erase the caller's reachable findings. Task handles remain distinct from their results, and awaiting a task requires its creating process and a single reply. The existing lifecycle and File-result policies keep their separate ownership contracts.

Unknown callback targets, other deferred consumers, cross-process task waits, unresolved timeout values and external code evaluation remain explicit boundaries. Static Erlang module calls are distinguished from dynamic module dispatch. Only a bounded, pure arithmetic subset of literal Code.eval_string payloads is modeled; arbitrary dynamic or call-bearing payloads and Code.eval_file do not become trusted merely because their arguments are literal strings.

C# Dapper SQL request flow

C# security category 8 includes cs.security.sql-injection in ordinary scans. It follows selected ASP.NET Core request inputs through aliases, branches and same-file helpers into Dapper's Execute, ExecuteScalar, Query, QueryFirst, QueryFirstOrDefault, QuerySingle and QuerySingleOrDefault families, including their asynchronous and supported Type-leading overloads. The scanner resolves ADO.NET connection types, Dapper imports and SqlMapper aliases before identifying an execution. Application methods with the same names are not automatically SQL sinks, and an arbitrary parameter named request is not an ASP.NET source.

SQL text and the separate parameter object retain distinct provenance. A fixed SQL template with request values bound to placeholders remains clean; supplying a parameter object cannot sanitize SQL text that already contains request data. Named arguments bind to their actual signature slots. JSON and SARIF findings point to the execution call and retain the source-to-sink trace.

Run ubs --only=csharp --format=json . or inspect the rule with ubs explain cs.security.sql-injection. No .NET compiler is required by the native rule. Category skipping, file selection, suppression and incremental cache policy apply normally. Its confidence remains unknown pending independent corpus calibration.

The bounded model covers selected text-SQL execution. CommandDefinition, deferred enumeration, multimapping, non-text command modes and unresolved request-bearing execution effects produce an explicit partial result. Known findings and independent files remain in the report, and partial scans are not cached as complete. The per-source limits are 100,000 tokens, 96 nesting levels and 500,000 steps, configurable through UBS_CSHARP_SQL_MAX_TOKENS, UBS_CSHARP_SQL_MAX_NESTING and UBS_CSHARP_SQL_MAX_STEPS.

PHP request flow and template output

PHP and PHTML files are scanned by a bounded native analyzer without requiring PHP or ast-grep to be installed. Six critical rules track request data into SQL, shell commands, dynamic code, includes, unsafe deserialization and HTML output. Use ubs --only=php --format=json . or inspect a rule with ubs explain php.security.sql-injection.

The analysis follows local assignments, selected helpers and request APIs, and distinguishes actual PDO/mysqli query receivers from unrelated methods with the same names. Prepared statements retain the SQL template captured at preparation; passing request data as bound values is safe only when that template is trusted. Output escaping is evaluated for its supported HTML context, rather than being a universal sanitizer for SQL, shell commands, URLs or script expressions.

PHP's five category IDs are php.sql, php.execution, php.includes, php.deserialization and php.output. Native rules appear in --list-rules and share ordinary file selection, exclusions, profiles, suppression, incremental cache and JSON/SARIF reporting. Custom ast-grep rules additionally require ast-grep. PHP native rules do not have an ast-grep --dump-rules representation.

This is selected PHP 8.1+ syntax and local flow, not whole-program framework resolution. Unsupported syntax, unresolved effects and exhausted analysis limits produce a partial scan while retaining findings already established. Default limits per source are 100,000 tokens, 128 nesting levels and 500,000 analysis steps, configurable through UBS_PHP_MAX_TOKENS, UBS_PHP_MAX_NESTING and UBS_PHP_MAX_STEPS. PHP confidence remains unknown pending independent corpus calibration; fixture coverage is not a calibrated confidence score.

PostgreSQL migration and function privileges

.sql, .psql, and .pgsql files use a bounded PostgreSQL DDL analyzer without requiring a database, PostgreSQL executable, or ast-grep:

ubs --only=sql --fail-on-warning --format=json migrations/
ubs --staged --format=sarif --fail-on-warning
ubs explain sql.security.definer-public-execute

Two warning rules check SECURITY DEFINER functions: function-local search paths that permit object shadowing, and PUBLIC EXECUTE exposure. The analyzer tracks literal input signatures, PostgreSQL type aliases, overloads, named/default/OUT parameters, selected ALTER FUNCTION changes, grants/revokes, default execution privileges, transactions and savepoints. Creating and restricting a function in one transaction stays clean; revoking access after a commit retains the earlier exposure finding. Revoking a grant option does not revoke execution.

Comments, quoted identifiers, strings and dollar-quoted bodies are separated lexically. A body containing fake DDL or suppression text cannot alter the outer migration. Actual SQL comments support ubs:ignore[rule-id]. SQL categories are sql.search-path (1) and sql.privileges (2); --skip-sql=1 and persistent .ubs.json policy use the same selection. JSON, JSONL, SARIF, text, staged scans, baseline filtering and the normal incremental cache use the shared runner.

The analysis covers each selected file in its own statement order. It does not infer migration order across files, execute function bodies, inspect a deployed catalog, or certify all database access controls. Fresh function defaults follow PostgreSQL's standard PUBLIC execution grant unless the selected migration changes it. Existing CREATE OR REPLACE ACLs, unproved schema CREATE privileges, role changes, dynamic SQL, psql includes, unsupported procedural DDL and malformed or budget-exhausted input produce explicit incomplete reports (exit 2). An empty search path is accepted only for the bounded SQL-body proof; unqualified relations still expose implicit temporary-schema lookup. Custom --rules for SQL are explicitly unsupported. No warning is emitted merely for ordinary DELETE or UPDATE statements. Limits are 4 MiB, 200,000 tokens, 10,000 statements and 2,000 function identities and 64 retained savepoints per source. Confidence remains uncalibrated.

The underlying privilege and search-path semantics follow PostgreSQL's CREATE FUNCTION security guidance, search_path documentation, and REVOKE semantics.

Terraform AWS configuration

Native .tf and generated .tf.json configuration files have four bounded AWS checks, without running Terraform, loading provider plugins, or contacting AWS:

ubs --only=hcl --fail-on-warning --format=json infrastructure/
ubs --staged --format=sarif --fail-on-warning
ubs explain hcl.aws.public-admin-ingress

hcl.aws.public-admin-ingress warns when an explicit world IPv4 or IPv6 CIDR allows TCP SSH/RDP ports (22/3389), including a containing port range, or all IP protocols. It supports native inline and attribute-style ingress on aws_security_group and aws_default_security_group, legacy aws_security_group_rule, and aws_vpc_security_group_ingress_rule. Public HTTP/HTTPS, UDP-only rules, restricted source networks and egress are separate from this administrative-ingress policy.

Ingress attributes may contain literal rule-object lists or selected local values in either syntax. Terraform's attribute-as-block conversion requires all nine rule fields; optional fields can use null. Empty or null collections add no explicit rule. Provider conversions preserve numeric protocol aliases and exact integer ports, including zero ports for inline all-protocol rules. A proven public rule produces one warning at its consuming ingress attribute, even when another row is unresolved. Unknown relevant values, invalid rows, or a mixture of ingress attributes and blocks make the report partial. Undeclared default-group rules and deployed prefix-list/group reachability are not inferred.

hcl.aws.public-s3-acl warns for explicit public canned ACLs on aws_s3_bucket and aws_s3_bucket_acl. A selected ignore_public_acls = true control must refer to the same bucket and provider to neutralize the warning. Conflicting ACL blocking, BucketOwnerEnforced, competing controls or unknown protection produce incomplete analysis, rather than a claim about deployed access. Missing account-wide controls and provider defaults are not inferred.

hcl.aws.iam-wildcard-privilege warns for unconditional identity-policy Allow statements that select "*" in both Action and Resource. It supports aws_iam_policy, aws_iam_role_policy, aws_iam_user_policy and aws_iam_group_policy with literal JSON strings, heredocs, or bounded jsonencode expressions over selected literals and locals. It also resolves .json and .minified_json from selected aws_iam_policy_document data sources in the same Terraform module, including local aliases, static action/resource lists and condition blocks, and the provider's default Allow effect. Static conditions group values by operator and context-variable name, preserving the provider's IAM-variable substitutions without evaluating condition truth. Findings point to the consuming resource's policy attribute; an unused data document does not create a finding. Static source_policy_documents and override_policy_documents follow the provider's statement-ID merge order: sources first, inline statements next, then overrides in list order. Matching nonblank Sid values replace whole statements; unnamed statements always survive. Duplicate named source or inline statements are invalid even when an override would replace them. Conditions and deny statements are checked after replacement; those still constraining a candidate grant produce incomplete analysis. Malformed JSON, duplicate keys, unsupported complement operators and unresolved policy documents also remain incomplete. The warning describes policy intent; it does not establish attachment, effective IAM permissions or permission-boundary state.

hcl.aws.insecure-provider-tls warns when an actual HashiCorp AWS provider configuration sets insecure = true, including provider aliases and renamed local provider bindings. Literal string booleans follow Terraform's primitive conversion; omitted or null values use the documented secure default. Unknown settings and duplicate provider identities remain incomplete. Unrelated provider plugins do not acquire AWS semantics from a similarly named option.

Selected files in the same directory share literal/local values and protective resource identities; nested directories remain distinct Terraform modules. Native and JSON files can provide configuration for each other. JSON files participate in automatic discovery, direct-file scans, and staged selections. The JSON frontend follows Terraform's expression and block mappings: a single ${...} interpolation retains its value type, ordinary reference-shaped strings remain literal, and provider references and literal settings keep their distinct semantics. Repeated block properties retain their order; duplicate attributes or evaluated expression keys are invalid. Unsupported mixed templates remain explicitly incomplete.

Changes to any selected context file invalidate cached results. Native #, // and block comments support scoped ubs:ignore[rule-id]; quoted strings, templates and heredocs cannot act as suppression comments. JSON "//" body properties are ignored as configuration comments, while "//" expression-object keys remain ordinary data. Categories are hcl.network (1), hcl.storage (2), hcl.iam (3) and hcl.tls (4), including --skip-hcl=3 and .ubs.json policy. All four rules emit warnings; --fail-on-warning or the strict profile makes them fail the scan.

This bounded model does not evaluate Terraform plans, state, remote modules, input variables, .tfvars/.tfvars.json, arbitrary functions or dynamic ingress. Relevant unresolved expressions, malformed syntax and exhausted budgets produce partial reports and exit 2 while retaining proven findings. Ordinary directory scans exclude downloaded .terraform dependencies. Source bounds are 4 MiB per file, 32 MiB per selected context, 200,000 tokens and 64 nesting levels. Local evaluation is memoized and bounded; JSON policy input and encoded output are capped at 256 KiB, including expansion of shared local expressions and intermediate merged documents. Generated JSON and repeated composition input parsing each have an additional 32 MiB budget per selected module. Ingress analysis shares memoized row, string-list and CIDR conversions, with module-wide limits of 4,096 selected rules and 200,000 work items. Custom --rules and extra non-.tf extensions are explicitly refused. Other providers, dynamic data statements, replicated data instances, arbitrary policy evaluation and deployed reachability remain outside this slice. Unresolved or cyclic data references stay incomplete. Confidence is uncalibrated.

The checks follow the AWS provider's ingress rule contract, S3 public-access controls, IAM policy grammar, IAM policy-document generation, AWS provider TLS setting, and Terraform's module file boundaries.

Shareable output quickstart

# 1) Capture a baseline JSON (checked into CI artifacts or local history)
ubs --ci --only=python --category=resource-lifecycle \
    --report-json .ubs/baseline.json test-suite/python/buggy

# 2) Re-run with comparison + HTML preview for PRs or chat threads
ubs --ci --only=python --category=resource-lifecycle \
    --comparison .ubs/baseline.json \
    --report-json .ubs/latest.json \
    --html-report  .ubs/latest.html \
    test-suite/python/buggy

latest.json now contains the git metadata (repo URL, commit, blob_base) plus a comparison.delta block, and latest.html renders a lightweight dashboard summarising the deltas. SARIF uploads also pick up the comparison id so repeating runs in CI stay grouped by automation id.

To publish SARIF and keep JSON and JSONL copies of the reported findings from one scan:

ubs . --ci --format=sarif --output=report.sarif \
    --also=json:report.json --also=jsonl:findings.jsonl

Every rendering shares the same findings, baseline filtering, and scan-completion state. Partial scans still export the available evidence and retain exit 2; an empty scan exports its explicit no-coverage result and retains exit 3. A failed additional export exits 2, names the failed path, preserves its previous report, and still attempts the other requested exports.

Detailed JSONL preserves the public canonical finding fields, including nested analyzer evidence in extras (such as an ordered extras.taint_path), project scope, explicit counts including zero, and suppression flags. JSONL uses language for the canonical lang field. Its type, project and timestamp describe the export; a finding's original timestamp becomes finding_timestamp. Evidence fields absent from the canonical finding remain absent.

With --new-only, an empty canonical findings array emits no finding records, even when scanner summaries retain previews from before baseline filtering. Ordinary legacy summaries still use their available finding previews. Their sample limits, rule thresholds and exit policy stay unchanged; JSONL export does not imply complete occurrence coverage for every analyzer or additivity for analyses that depend on cross-file context. The separate Python pattern-count export defines its own distinct-line count and completeness contract. Request --all-occurrences when a consumer needs every available source occurrence and an explicit check that none are missing.


💡 Basic Usage

# Scan current directory
ubs .

# Scan specific directory
ubs /path/to/your/project

# Verbose mode (show more code examples)
ubs -v .

# Save report to file
ubs . bug-report.txt

# CI mode (exit code 1 on warnings)
ubs . --fail-on-warning

# Quiet mode (summary only)
ubs -q .

# Skip specific categories (by stable id or category number)
ubs . --skip=js.debug,python.todo

# Custom file extensions
ubs . --include-ext=js,ts,vue,svelte

Handy switches

# Git-aware quick scans (changed files only)
ubs --staged    # Scan files staged for commit
ubs --diff      # Scan working tree changes vs HEAD

# Strictness profiles
ubs --profile=strict   # Fail on warnings, enforce high standards
ubs --profile=loose    # Skip TODO/debug/code-quality nits when prototyping

# Machine-readable output
ubs . --format=json    # Pure JSON on stdout; logs go to stderr
ubs . --format=jsonl   # Line-delimited summary per scanner + totals
ubs . --format=toon    # TOON format (~50% smaller than JSON, LLM-optimized); exit 2 + envelope when the toon_rust encoder is missing
ubs . --format=jsonl --beads-jsonl out/findings.jsonl  # Save JSONL for Beads/"strung"

Keeping noise low

  • UBS auto-ignores common junk (node_modules, virtualenvs, dist/build/target/vendor, editor caches, etc.).
  • Inline suppression is available when a finding is intentional: eval("print('safe')") # ubs:ignore

🚀 Quick Install (30 Seconds)

Recommended: Homebrew (macOS/Linux)

brew install dicklesworthstone/tap/ubs

This method provides:

  • Automatic updates via brew upgrade
  • Dependency management
  • Easy uninstall via brew uninstall

Alternative: Automated Install

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/install.sh?$(date +%s)" | bash

Option 2: Integrity-first install (signed checksums)

export UBS_MINISIGN_PUBKEY="RWS+jJ7psytzl3v4znpraY9VWBQrICXBFmT3VwvxpTzbuV2Q/CBTDmVJ"
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/scripts/verify.sh | bash

The verifier downloads SHA256SUMS from the matching release and validates its signature — with minisign when UBS_MINISIGN_PUBKEY is set, otherwise with cosign verify-blob against the release's keyless Sigstore bundle (SHA256SUMS.sigstore.json, so no key to distribute) — checks install.sh, and only then executes it. UBS_VERIFY_WITH=minisign|cosign forces a path; --insecure bypasses verification (not recommended).

Option 3: Nix

Run directly (no install):

nix run github:Dicklesworthstone/ultimate_bug_scanner

Dev shell for contributors:

nix develop

Option 4: Docker / OCI

Pull & inspect:

docker run --rm ghcr.io/dicklesworthstone/ubs-tools ubs --help

Scan host code (risk-aware: grants container access to host FS):

docker run --rm -v /:/host ghcr.io/dicklesworthstone/ubs-tools bash -c "cd /host/path && ubs ."

⚠️ Use the host-mount pattern only when you understand the write-access implications.

[!NOTE] The repository Dockerfile (debian:trixie-slim) installs bash, curl, git, jq, ripgrep, rsync, python3, and unzip, and pre-provisions the digest-verified ast-grep binary and language modules at build time for offline container scans. Verify any published registry tag against the release digest before assuming parity with the checkout Dockerfile.

Deployment & Security

The installer will:

  • ✅ Install the ubs command globally
  • ✅ Install/ensure ast-grep (required for accurate JS/TS scanning; UBS can auto-provision a pinned binary)
  • ✅ Optionally install ripgrep (for 10x faster scanning)
  • ✅ Optionally install jq (needed for JSON/SARIF merging across all language scanners)
  • ✅ Optionally install typos (smart spellchecker for docs and identifiers)
  • ✅ Optionally install toon_rust's toon encoder for --format=toon (digest-pinned release binary; cargo install tru as the fallback)
  • ✅ Optionally install Node.js + typescript (enables deep TypeScript type narrowing analysis)
  • ✅ Auto-run ubs doctor post-install and append a session summary to ~/.config/ubs/session.md
  • ✅ Capture readiness facts (ripgrep/jq/typos/toon/type narrowing) and store them for ubs sessions --entries 1
  • ✅ Set up git hooks (block commits with critical bugs)
  • ✅ Set up Claude Code hooks (scan on file save)
  • ✅ Add documentation to your AGENTS.md

Need to revisit what the installer discovered later? Run ubs sessions --entries 1 to view the most recent session log (or point teammates at the same summary).

Need the “just make it work” button? Run the installer with --easy-mode to auto-install every dependency, accept all prompts, detect local coding agents, and wire their quality guardrails with zero extra questions:

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/install.sh?$(date +%s)" \
  | bash -s -- --easy-mode

Total time: 30 seconds to 2 minutes (depending on dependencies)

Need to keep your shell RC files untouched? Combine --no-path-modify (and optionally --skip-hooks) with the command above—the installer will still drop ubs into your chosen --install-dir, but it will skip both PATH edits and the alias helper entirely.

Option 2: Manual Install

# Download and install the unified runner
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/ubs \
  -o /usr/local/bin/ubs && chmod +x /usr/local/bin/ubs

# Verify it works
ubs --help

# Install dependencies (ast-grep required for JS/TS scanning)
# Required for JS/TS scanning (syntax-aware AST engine)
brew install ast-grep            # or: cargo install ast-grep, npm i -g @ast-grep/cli
brew install ripgrep             # 10x faster searching (or: apt/dnf/cargo install)
brew install typos-cli           # Spellchecker tuned for code (or: cargo install typos-cli)
npm install -g typescript        # Enables full tsserver-based type narrowing checks

Option 3: Use Without Installing

# Download once
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/ubs \
  -o ubs && chmod +x ubs

# Run it
./ubs .

Installer Safety Nets

Uninstall from any shell

Run the installer in --uninstall mode via curl if you want to remove UBS and all of its integrations:

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/install.sh?$(date +%s)" | bash -s -- --uninstall --non-interactive

This command deletes the UBS binary, shell RC snippets/aliases, config under ~/.config/ubs, and the optional Claude/Git hooks that the installer set up. Because it passes --non-interactive, it auto-confirms all prompts and runs unattended.

Flag What it does Why it matters
--dry-run Prints every install action (downloads, PATH edits, hook writes, cleanup) without touching disk. Dry runs still resolve config, detect agents, and show you exactly what would change. Audit the installer, demo it to teammates, or validate CI steps without modifying a workstation.
--self-test Immediately runs test-suite/install/run_tests.sh after installation and exits non-zero if the smoke suite fails. CI/CD jobs and verified setups can prove the installer still works end-to-end before trusting a release.
--skip-type-narrowing Skip the Node.js + TypeScript readiness probe and the cross-language guard analyzers (JS/Rust/Kotlin/Swift/C#). Useful for air-gapped hosts or environments that want to stay in heuristic-only mode.
--skip-toon Skip installing toon_rust's toon encoder, the digest-verified release binary that --format=toon needs. Non-interactive installs fetch it by default; interactive ones ask. For hosts that already ship toon/tru, or when you never use TOON output. ubs doctor --fix can fetch it later.
--skip-typos Skip the Typos spellchecker installation + diagnostics. Handy when corp images already provide Typos or when you deliberately disable spellcheck automation.
--skip-doctor Skip the automatic ubs doctor run + session summary after install. Use when CI already runs doctor separately or when you're iterating locally and want a faster finish.

[!WARNING] --self-test requires running install.sh from a working tree that contains test-suite/install/run_tests.sh (i.e., the repo root). Curl-piping the installer from GitHub can’t self-test because the harness isn’t present, so the flag will error out early instead of giving a false sense of safety.

[!NOTE] After every install the script now double-checks command -v ubs. If another copy shadows the freshly written binary, you’ll get an explicit warning with both paths so you can fix PATH order before running scans.

[!TIP] Type narrowing relies on Node.js plus the typescript npm package and the Python helpers that power the Rust/Kotlin/Swift/C# checks. The installer now checks Node/TypeScript readiness, can optionally run npm install -g typescript, and surfaces the status inside install.sh --diagnose. Use --skip-type-narrowing if you’re on an air-gapped host or plan to keep the heuristic-only mode.

[!TIP] To avoid global npm permission issues, the installer now detects/installs bun just like other dependencies and uses bun install --global typescript by default, falling back to npm only if bun isn’t available.

The diagnostics also call out Swift guard readiness: if python3 is available we count .swift files under your repo and record whether the guard helper will actually run. That fact shows up in install.sh --diagnose output and the auto-generated session log so iOS/macOS teams can tell at a glance whether the ObjC-bridging heuristics are active.

Common combos

# Preview everything without touching dotfiles or hooks
bash install.sh --dry-run --no-path-modify --skip-hooks --non-interactive

# CI-friendly install that self-tests the smoke harness
bash install.sh --easy-mode --self-test --skip-hooks

# Install the ubs in the current directory (a checkout's install.sh does this by itself;
# without --local a stray ./ubs is ignored and the verified release is installed)
bash install.sh --local --skip-hooks

Dependency binaries the installer downloads (ast-grep, jq, typos, ripgrep, toon) are verified against pinned SHA-256 digests or the upstream .sha256 sidecar before they are installed; a mismatch aborts that install, and --skip-verification prints exactly which check it skipped.

🔄 Auto-Update

The ubs meta-runner supports an opt-in auto-update check (once every 24 hours). This is disabled by default for supply-chain safety.

To enable auto-update:

export UBS_ENABLE_AUTO_UPDATE=1

To disable it (even if enabled):

export UBS_NO_AUTO_UPDATE=1
# or
ubs --no-auto-update .

Ultimate Bug Scanner is like having a senior developer review every line of code in under 5 seconds; it's the perfect automated companion to your favorite coding agent:

$ ubs .

╔══════════════════════════════════════════════════════════════════════╗
║  🔬 ULTIMATE BUG SCANNER v5.3 - Scanning your project...             ║
╚══════════════════════════════════════════════════════════════════════╝

Project:  /Users/you/awesome-app
Files:    247 JS/TS + 58 Python + 24 Go + 16 Java + 11 Ruby + 12 C++/Rust files
Finished: 3.2 seconds

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Summary Statistics:
  Files scanned:    247
  🔥 Critical:      0    ← Would have crashed in production!
  ⚠️  Warnings:      8    ← Should fix before shipping
  ℹ️  Info:          23   ← Code quality improvements

✨ EXCELLENT! No critical issues found ✨

⚡ Why Developers + AI Agents Will Love This Tool

🚀 1. Catches What Humans & AI Miss

18 specialized detection categories covering the bugs that actually matter:

Category What It Prevents Time Saved Per Bug
Null Safety "Cannot read property of undefined" crashes 2-4 hours
Security Holes XSS, code injection, prototype pollution 8-20 hours + reputation damage
Async/Await Bugs Race conditions, unhandled rejections 4-8 hours
Memory Leaks Event listeners, timers, detached DOM 6-12 hours
Type Coercion JavaScript's === vs == madness 1-3 hours
+ 13 more categories 100+ hours/month saved

💨 2. Fast Enough for the Edit Loop When You Scope It

scripts/bench.sh also records the agent-hook case as single_file: the meta-runner on one file (--ci, text mode) against the Python module alone on the same file, overhead p50/p95 in ms (target p95 ≤ 150 ms, tracked by scripts/bench_cusum.py).

Measured on 2026-09-02 (v5.3.13, single runs on a 16-core Linux box; ast-grep on PATH). Reproduce with scripts/bench.sh (fixture tree + a deterministic synthetic 50K-line corpus; --large, --oss for more) and watch for regressions with scripts/bench_cusum.py; UBS_PROFILE=1 prints per-phase timings:

Single file (JS or Python):                   4.6–5.8 seconds
15-file repository (this one):                9 seconds
29K-line fixture tree before PHP support:    40 seconds
124K-line Python project (597 files):         143 seconds
407K-line TypeScript monorepo (--only=js):    hit the 300 s module timeout

Roughly 700–900 lines per second today; the performance program that closes the gap to the original "10,000 lines/second" target is tracked as bead ultimate_bug_scanner-q150 (profiling, a literal prefilter, an incremental cache). Until then, scope scans to the files you changed (ubs --staged, ubs <file>), which is the workflow the agent integrations use.

🤖 3. Built FOR AI Agents, BY Developers Who Use AI

Unlike traditional linters that fight AI-generated code, this scanner embraces it:

✅ Designed for Claude Code, Cursor, Windsurf, Aider, Continue, Copilot
✅ Zero configuration - works with ANY JS/TS, Python, C/C++, Rust, Go, Java, or Ruby project
✅ Integrates with git hooks, CI/CD, file watchers
✅ Actionable output (tells you WHAT's wrong and HOW to fix it)
✅ Fails fast in CI (catch bugs before they merge)
✅ React Hooks dependency analysis that spots missing deps, unstable objects, and stale closures
✅ Lightweight taint analysis that traces req.body/window.location/localStorage → innerHTML/res.send/eval/exec/db.query and flags flows without DOMPurify/escapeHtml/parameterized SQL. Imported Drizzle `sql` templates retain parameter binding through storage, aliases, nested templates, closures, and selected local or cross-file helper returns. Genuine SQL objects retain their identities through selected helpers: `sql.empty()`, `sql.raw()`, and `append()` preserve raw SQL provenance, copied chunk entries, and live nested query references. `sql.join()` copies its outer list entries, while `sql.fromList()` retains the original mutable list; both preserve live nested SQL and array references. Scalar chunks and separators bind only when the SQL object is rendered, so using the original input elsewhere still carries its ordinary taint. Literal `sql.identifier()` names can be composed; variable identifier names remain conservative. Custom `SQLWrapper`/`getSQL()` objects, unknown array protocols, accessors, escaped objects, and non-scalar raw conversion produce explicit incomplete results while retaining proven findings. The separate SQL detector uses bounded, exact call proofs from the same engine to clear safe composition findings, preserving independent and nested unsafe executions.

📊 4. Real-World Impact

Scenario Without Scanner With Scanner
AI implements user auth • 3 null pointer crashes (9h debugging)
• 1 XSS vulnerability (8h + incident)
• 2 race conditions (4h debugging)
Total: ~21 hours + security incident
• All issues caught in 4 seconds
• Fixed before commit (15 min)
Total: 15 minutes
Savings: 84x faster ⚡
Refactor payment flow • Division by zero in edge case (3h)
• Unhandled promise rejection (2h)
• Missing error logging (1h)
Total: 6 hours debugging
• Caught instantly (3 sec)
• Fixed before merge (10 min)
Total: 10 minutes
Savings: 36x faster 🚀

🤖 AI Agent Integration (The Real Magic)

On-Device Agent Guardrails

install.sh now inspects your workstation for the most common coding agents (the same set listed below) and, when asked, drops guardrails that remind those agents to run ubs --fail-on-warning . before claiming a task is done. In --easy-mode this happens automatically; otherwise you can approve each integration individually.

Agent / IDE What we wire up Why it helps
Claude Code Desktop (.claude/hooks/on-file-write.sh) File-save hook that shells out to ubs --ci whenever Claude saves JS/TS files. Keeps Claude from accepting “Apply Patch” without a fresh scan.
Cursor (.cursor/rules) Shared rule block that tells Cursor plans/tasks to run ubs --fail-on-warning . and summarize outstanding issues. Cursor’s autonomous jobs inherit the same QA checklist as humans.
Codex CLI (.codex/rules/ubs.md) Adds the identical rule block for OpenAI's Codex terminal workflow. Supports both file and directory formats (v0.77.0+). Ensures Codex sessions never skip the scanner during long refactors.
Gemini Code Assist (.gemini/rules) Guidance instructing Gemini agents to run ubs before closing a ticket. Keeps Gemini’s asynchronous fixes aligned with UBS exit criteria.
Windsurf (.windsurf/rules) Guardrail text + sample command palette snippet referencing ubs. Windsurf’s multi-step plans stay grounded in the same quality gate.
Cline (.cline/rules) Markdown instructions that Cline’s VS Code extension ingests. Forces every “tool call” from Cline to mention scanner findings.
OpenCode MCP (.opencode/rules) Local MCP instructions so HTTP tooling always calls ubs before replying. Makes OpenCode's multi-agent swarms share the same notion of "done".

Codex CLI v0.77.0+ Migration Note

Starting with Codex CLI v0.77.0, the rules storage changed from a single file (.codex/rules) to a directory (.codex/rules/) containing individual rule files. The UBS installer handles both formats automatically:

Codex Version Rules Location UBS Installer Behavior
< v0.77.0 .codex/rules (file) Appends UBS quick reference to file
≥ v0.77.0 .codex/rules/ (directory) Creates .codex/rules/ubs.md

If you upgraded Codex and encounter issues, migrate manually:

# Convert file to directory structure
mv ~/.codex/rules ~/.codex/rules.backup
mkdir ~/.codex/rules
mv ~/.codex/rules.backup ~/.codex/rules/ubs.md

The installer's append_quick_reference_block() function detects the storage format at runtime and writes to the appropriate location, so re-running install.sh after upgrading Codex will "just work."

Why This Matters for AI Workflows

When you're coding with AI, you're moving 10-100x faster than traditional development. But bugs accumulate just as quickly. Traditional tools slow you down. This scanner keeps pace:

Traditional workflow:              AI-powered workflow with scanner:
┌──────────────────┐              ┌──────────────────┐
│ AI writes code   │              │ AI writes code   │
└────────┬─────────┘              └────────┬─────────┘
         │                                 │
         ↓                                 ↓
┌──────────────────┐              ┌──────────────────┐
│ You review       │              │ Scanner runs     │
│ (15 min)         │              │ (3 seconds)      │
└────────┬─────────┘              └────────┬─────────┘
         │                                 │
         ↓                                 ↓
┌──────────────────┐              ┌──────────────────┐
│ Tests pass?      │              │ Critical bugs?   │
└────────┬─────────┘              └────────┬─────────┘
         │ NO!                              │ YES!
         ↓                                 ↓
┌──────────────────┐              ┌──────────────────┐
│ Debug in prod    │              │ AI fixes them    │
│ (6 hours)        │              │ (5 minutes)      │
└──────────────────┘              └────────┬─────────┘
                                           ↓
                                  ┌──────────────────┐
                                  │ Ship with         │
                                  │ confidence        │
                                  └──────────────────┘

Total: 6.25 hours                Total: 8 minutes

Pattern 1: Claude Code Integration (Real-Time Scanning)

The installer (--easy-mode, or install.sh --setup-claude-hook inside a project) writes .claude/hooks/on-file-write.sh and registers it in .claude/settings.json as a PostToolUse hook on Edit|Write|MultiEdit, alongside the PreToolUse safety guard:

{
  "hooks": {
    "PostToolUse": [
      { "matcher": "Edit|Write|MultiEdit",
        "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/on-file-write.sh" } ] }
    ],
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/git_safety_guard.py" } ] }
    ]
  }
}

The hook reads the tool call Claude Code passes on stdin, scans only the file that was just written (ubs <file> --ci), and exits 2 with the findings on stderr when the scan reports critical issues, so Claude sees them as feedback on the edit it just made. Clean files and non-source files exit 0 silently. Re-running the installer never duplicates the registration; your other settings are preserved and the previous file is backed up.

Result: Every time Claude writes code, the scanner catches bugs instantly.

Pattern 2: Git Pre-Commit Hook (Quality Gate)

The installer can set this up automatically, or add to .git/hooks/pre-commit:

#!/bin/bash
# Block commits with critical bugs

echo "🔬 Running bug scanner..."

if ! ubs . --fail-on-warning 2>&1 | tee /tmp/scan.txt | tail -30; then
  echo ""
  echo "❌ Critical issues found. Fix them or use: git commit --no-verify"
  echo ""
  echo "Top issues:"
  grep -A 3 "🔥 CRITICAL" /tmp/scan.txt | head -20
  exit 1
fi

echo "✅ Quality check passed - committing..."

Result: Bugs cannot be committed. Period.

Pattern 3: Cursor/Windsurf/Continue Integration

Add to your .cursorrules or similar:

## Code Quality Standards

Before marking any task as complete:

1. Run the bug scanner: `ubs .`
2. Fix ALL critical issues (🔥)
3. Review warnings (⚠️) and fix if trivial
4. Only then mark task complete

If the scanner finds critical issues, your task is NOT done.

Result: AI agents have built-in quality standards.

Pattern 4: CI/CD Pipeline (GitHub Actions Example)

name: Code Quality Gate

on: [push, pull_request]

jobs:
  bug-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install Bug Scanner
        run: |
          curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/install.sh?$(date +%s)" | bash -s -- --non-interactive

      - name: Scan for Bugs
        run: |
          ubs . --fail-on-warning --ci

Result: Pull requests with critical bugs cannot merge.

Pattern 5: The Fix-Verify Loop (For AI Agents)

This is the golden pattern for AI coding workflows:

#!/bin/bash
# Have your AI agent run this after implementing features

echo "🔬 Post-implementation quality check..."

# Run scanner
if ubs . --fail-on-warning > /tmp/scan-result.txt 2>&1; then
  echo "✅ All quality checks passed!"
  echo "📝 Ready to commit"
  exit 0
else
  echo "❌ Issues found:"
  echo ""

  # Show critical issues
  grep -A 5 "🔥 CRITICAL" /tmp/scan-result.txt | head -30

  echo ""
  echo "🤖 AI: Please fix these issues and re-run this check"
  exit 1
fi

Usage pattern:

User: "Add user registration with email validation"

AI Agent:
1. Implements the feature
2. Runs quality check (scanner finds 3 critical bugs)
3. Fixes the bugs
4. Re-runs quality check (passes)
5. Commits the code

Total time: 12 minutes (vs. 6 hours debugging in production)

Pattern 6: The "AI Agent Decision Tree"

Train your AI agent to use this decision tree:

Did I modify code in any supported language?
(JS/TS, Python, Go, Rust, Java, C++, Ruby)
         │
         ↓ YES
Changed more than 50 lines?
         │
         ↓ YES
    Run scanner ←──────────┐
         │                 │
         ↓                 │
Critical issues found? ────┤ YES
         │ NO              │
         ↓                 │
     Warnings?             │
         │                 │
         ↓ YES             │
  Show to user             │
  Ask if should fix ───────┤
         │ NO              │
         ↓                 ↓
    Commit code      Fix issues

[!IMPORTANT] Copy the blurb below to your project's AGENTS.md, .claude/claude_docs/, or .cursorrules file for comprehensive UBS integration guidance.

## UBS Quick Reference for AI Agents

UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On**

**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/install.sh | bash`

**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.

**Commands:**
```bash
ubs file.ts file2.py                    # Specific files (< 1s) — USE THIS
ubs $(git diff --name-only --cached)    # Staged files — before commit
ubs --only=js,python src/               # Language filter (3-5x faster)
ubs --ci --fail-on-warning .            # CI mode — before PR
ubs --help                              # Full command reference
ubs sessions --entries 1                # Tail the latest install session log
ubs .                                   # Whole project (ignores things like .venv and node_modules automatically)
```

**Output Format:**
```
⚠️  Category (N errors)
    file.ts:42:5 – Issue description
    💡 Suggested fix
Exit code: 1
```
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail | Exit 2 → environment/usage error (fix the invocation or `ubs doctor --fix`) | Exit 3 → nothing was scanned (not a pass)

**Fix Workflow:**
1. Read finding → category + fix suggestion
2. Navigate `file:line:col` → view context
3. Verify real issue (not false positive)
4. Fix root cause (not symptom)
5. Re-run `ubs <file>` → exit 0
6. Commit

**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits.

**Bug Severity:**
- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks
- **Important** (production): Type narrowing, division-by-zero, resource leaks
- **Contextual** (judgment): TODO/FIXME, console logs

**Anti-Patterns:**
- ❌ Ignore findings → ✅ Investigate each
- ❌ Full scan per edit → ✅ Scope to file
- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`)

🎬 See It In Action

Examples show JavaScript output; each language has equivalent detections (Python: None checks, Go: nil guards, Rust: Option handling, etc.)

Example 1: Catching a Null Pointer Bug

$ ubs src/

▓▓▓ NULL SAFETY & DEFENSIVE PROGRAMMING
Detects: Null pointer dereferences, missing guards, unsafe property access

  🔥 CRITICAL (5 found)
    Unguarded property access after getElementById
    Consider: const el = document.getElementById('x'); if (!el) return;

      src/components/form.js:42
        const submitBtn = document.getElementById('submit-button');
        submitBtn.classList.add('active');  // ← Crashes if element missing

      src/utils/dom.js:87
        const modal = document.querySelector('.modal');
        modal.style.display = 'block';  // ← Runtime crash guaranteed

  💡 Fix: Always check for null before accessing properties

Before: 3 production crashes this week After: 0 crashes, caught in 2 seconds

Example 2: Security Vulnerability Detection

▓▓▓ SECURITY VULNERABILITIES
Detects: Code injection, XSS, prototype pollution, timing attacks

  🔥 CRITICAL (3 found)
    innerHTML without sanitization - XSS risk
    Use textContent or DOMPurify.sanitize()

      src/comments.js:156
        element.innerHTML = userComment;  // ← XSS vulnerability!

  🔥 CRITICAL (1 found)
    Hardcoded API keys detected
    Use environment variables or secret managers

      src/config.js:23
        const apiKey = "sk_live_abc123xyz";  // ← Security breach!

Before: Security incident, customer data at risk After: Vulnerability caught before git commit

Example 3: Async/Await Gotchas

▓▓▓ ASYNC/AWAIT & PROMISE PITFALLS
Detects: Missing await, unhandled rejections, race conditions

  🔥 CRITICAL (8 found)
    await used in non-async function
    SyntaxError in JavaScript

      src/api/users.js:67
        function saveUser(data) {
          await database.insert(data);  // ← SyntaxError!
        }

  ⚠️  WARNING (12 found)
    Promises without .catch() or try/catch
    Unhandled rejections crash Node.js

      src/services/email.js:45
        sendEmail(user.email).then(result => ...)  // ← No error handling!

Before: Silent failures, mysterious bugs in production After: All async bugs caught and fixed before deploy


📋 What It Detects (The Complete Arsenal)

Each language module has specialized detections. Examples below are representative (JavaScript shown; Python has eval(), Go has goroutine leaks, Rust has .unwrap() panics, C++ has buffer overflows, etc.)

🔴 Critical Issues (Production Blockers)

These WILL cause crashes, security breaches, or data corruption:

Pattern Example Why It's Dangerous
eval() usage eval(userInput) Allows arbitrary code execution - RCE vulnerability
Direct NaN comparison if (x === NaN) Always returns false - logic bug
Missing await asyncFunc() in async context Silent failures, race conditions - data corruption
Prototype pollution obj.__proto__ = {} Security vulnerability - privilege escalation
Unguarded null access el.style.color without null check Runtime crash guaranteed
parseInt without radix parseInt("08") Returns 0 in some browsers - calculation bug
Empty catch blocks catch(e) {} Swallows errors - debugging nightmare
innerHTML with user data el.innerHTML = userInput XSS vulnerability
Missing async keyword await without async function SyntaxError
Hardcoded secrets const key = "sk_live..." Security breach

🟡 Warnings (Should Fix Before Shipping)

These cause bugs, performance issues, or maintenance headaches:

Pattern Example Impact
Promises without .catch() promise.then(...) Unhandled rejections crash Node.js
Division without zero check total / count Returns Infinity or NaN
Event listeners without cleanup addEventListener in React Memory leak (app gets slower over time)
setInterval without clear setInterval(fn, 1000) Timer leak (infinite timers)
fetch() without cancellation fetch(url) Stalled requests can hang workflows and exhaust resources
await inside loops for(...) { await api.call() } Slow (sequential, not parallel)
Array mutation during iteration arr.forEach(() => arr.push(...)) Skipped/duplicate elements
Missing switch default switch(x) { case 1: ... } Unhandled values cause silent failures
isNaN() instead of Number.isNaN() isNaN("foo") Type coercion bugs

🔵 Info (Code Quality & Best Practices)

Improvements that make code cleaner and more maintainable:

  • Optional chaining opportunities (obj?.prop?.value)
  • Nullish coalescing opportunities (value ?? default)
  • TypeScript any usage (reduces type safety)
  • console.log statements (remove before production)
  • Technical debt markers (TODO, FIXME, HACK)
  • Performance optimizations (DOM queries in loops)
  • var usage (use let/const instead)
  • Deep property access without guards
  • Large inline arrays (move to separate files)
  • Complex nested ternaries (readability)

⚙️ Advanced Configuration

Command-Line Options (Full Reference)

ubs [OPTIONS] [PROJECT_DIR] [OUTPUT_FILE]

Core Options:
  -v, --verbose            Show 10 code samples per finding (default: 3)
  -q, --quiet              Minimal output (summary only)
  --ci                     CI mode (stable output, no colors by default)
  --fail-on-warning        Exit with code 1 on warnings (strict mode)
  --version                Print UBS meta-runner version and exit
  --profile=MODE           strict|loose (sets defaults for strictness)
  --config=PATH            Load an explicit JSON scan policy instead of project .ubs.json
  --no-config              Disable automatic project configuration
  --baseline=FILE          Compare findings against a baseline JSON (alias for --comparison)
  -h, --help               Show help and exit

Git Integration:
  --staged                 Scan only files staged for commit
  --diff, --git-diff       Scan only modified files (working tree vs HEAD)

Output Control:
  --format=FMT             Output format: text|json|jsonl|sarif|toon (default: text)
  --beads-jsonl=FILE      Write JSONL summary alongside normal output for Beads/"strung"
  --all-occurrences      JSONL source occurrences, code, stable IDs and completeness
  --max-occurrences=N    Scan-wide occurrence export limit (default 100000; max 1000000)
  --python-pattern-counts=FILE  Write unthresholded Python pattern counts as JSON
  --no-color               Force disable ANSI colors
  OUTPUT_FILE              Save report to file (auto-tees to stdout)

File Selection:
  --include-ext=CSV        File extensions (default: auto-detect by language)
                           JS: js,jsx,ts,tsx,mjs,cjs | Python: py,pyi,pyx
                           Go: go | Rust: rs | Java: java | C++: cpp,cc,cxx,c,h
                           Ruby: rb,rake,ru | C#: cs,csx | Custom: --include-ext=js,ts,vue
  --exclude=GLOB[,..]      Skip PATHS: globs/dirs with .ubsignore semantics (e.g.
                           --exclude=legacy,generated,*.min.js), merged with .ubsignore
                           and the default ignores and forwarded to every module.
                           A value that names a language (js, python, …) with no such
                           path is rejected with exit 2 — use --exclude-langs for that.
  --exclude-langs=CSV      Exclude LANGUAGES from the scan (e.g. --exclude-langs=js,ruby)
  --output=FILE, -o FILE   Also write the report to FILE (same as the positional
                           OUTPUT_FILE form); stdout still receives it
  --also=FMT:FILE          Also render this scan as json|jsonl|sarif|toon to FILE;
                           repeatable, without rescanning any module
  --skip-size-check        Skip directory size guard (use with care)

Performance:
  --jobs=N                 Parallel jobs for ripgrep (default: auto-detect cores)
                           Set to 1 for deterministic output

Rule Control:
  --skip=CSV               Skip categories by id or number (see --list-categories)
                           Example: --skip=js.debug,python.todo  # Skip debug code + TODOs
  --skip-type-narrowing    Disable helper-backed guard analysis (falls back to text heuristics)
  --no-cargo               Rust: static analysis only. Skips every cargo phase (categories 12-14:
                           fmt/clippy, check/test --no-run, audit/deny/udeps/outdated). Each skipped
                           category reports "Not evaluated" instead of "clean". Cargo phases skip
                           when the selection has no Cargo.toml; a staged manifest uses a complete
                           index snapshot for Cargo while static checks keep their changed-file scope.
  --rules=DIR              Additional ast-grep rules directory
                           Rules are merged with built-in rules; repeatable
  --no-auto-update         Disable automatic self-update
  --suggest-ignore         Print large-directory candidates to add to .ubsignore (no changes applied)

Environment Variables:
  JOBS                     Same as --jobs=N
  UBS_SKIP_RUST_BUILD=1    Same as --no-cargo (Rust static-only mode)
  UBS_RULES=DIR[:DIR...]   Custom rule directories used when no --rules is given
  UBS_SKIP_CATEGORIES=CSV Global category skips; project < environment < --skip
  NO_COLOR                 Disable colors (respects standard)
  CI                       Enable CI mode automatically
  UBS_MAX_DIR_SIZE_MB      Max directory size in MB before refusing to scan (default: 1000)
  UBS_MAX_FILE_MB          Leave files above N MB out of whole-project scans (default: 8; 0 disables)
  UBS_PYTHON               Python >= 3.9 interpreter to use as python3 (Git Bash/Windows: `python` or `py -3` are tried automatically)
  UBS_PYTHON_FLOW_MAX_STEPS   Override each Python taint/header analyzer's per-file work limit (positive integer)
  UBS_PYTHON_FLOW_SCOPE_STEPS Override cumulative work per Python function, including callback contexts (positive integer)
  UBS_ALLOW_PARTIAL=1      Accept partial runs (timed-out/crashed module): exit on findings, not 2
  UBS_PROFILE=1            Add phase timings (list, fan-out, per module, merge, total ms) to json/toon output and the text summary
                           Numeric 0/1 leaves project severity unchanged; strict|loose overrides it
  UBS_SKIP_SIZE_CHECK      Skip directory size guard entirely (set to 1)
  UBS_ALLOW_NO_SCAN        Exit 0 instead of 3 when nothing was scanned (set to 1)
  UBS_KEEP_TEMP=1          Retain scanner scratch for Rust/Python/JS/Bash/HCL/C#/Kotlin; see below

Arguments:
  PROJECT_DIR              Directory to scan (default: current directory)
  OUTPUT_FILE              Save full report to file

Exit Codes:
  0                        No critical issues (or no issues at all)
  1                        Critical issues found
  1                        Warnings found (only with --fail-on-warning)
  2                        Invalid arguments, environment error (e.g., missing ast-grep for JS/TS),
                           a refused scan (directory too large, $HOME or /), or a PARTIAL run in
                           which a scanner module timed out, crashed, or could not finish one of
                           its own analysis layers (module_error ANALYZER_ERROR). Set
                           UBS_ALLOW_PARTIAL=1 to exit on findings alone for partial runs.
  3                        Nothing was scanned: no supported languages detected, so no scanner ran.
                           This is NOT a pass. Set UBS_ALLOW_NO_SCAN=1 to restore the legacy exit-0
                           behaviour for callers that intentionally scan mixed/unsupported trees.

Rust context in staged scans

ubs --staged analyzes the indexed versions of changed files. When that selection includes Cargo.toml at the selected directory's root, Cargo phases use a separate snapshot of all regular files in the index, including unchanged modules, workspace members, local dependencies, build inputs, and .cargo configuration. Unstaged and untracked files cannot change that snapshot. Static checks still scan only the selected changed files. If indexed symlinks or submodules prevent a complete Cargo snapshot, Cargo phases report the missing context instead of using the worktree. Use --no-cargo for static analysis alone.

Retaining scan scratch

Set UBS_KEEP_TEMP=1 to retain scanner-owned workspaces, rule files, captured tool output, merge staging, and spill databases after successful, failed, or interrupted Rust, Python, JavaScript/TypeScript, Bash, Terraform HCL, C#, and Kotlin scans. Set TMPDIR to an existing writable directory to choose where scratch accumulates; retained files can contain source code and findings, so use private storage. The caller manages its later removal. Retention does not change detectors, severity, module verification, or scan exit codes, and it disables scan-cache pruning. Staged scans read immutable blob IDs directly from the index, retaining the indexed version even when the working file differs. They reject unmerged entries and avoid Git's temporary-object cleanup during tree creation.

This mode requires Bash >= 5.1 in its modern compatibility mode and supports static analysis. Older Bash creates and unlinks heredoc backing files internally. Select --no-cargo for Rust projects with enabled Cargo phases and --no-uv for direct Python scans that would run optional auditors. Build and auditor processes manage their own temporary files and are refused rather than silently skipped. Other languages, service and doctor commands, forced updates, and automatic tool installation are outside this contract and refuse with exit 2. Install required tools before scanning. Ordinary scans without this environment variable retain their existing cleanup behavior. Retention does not authorize overwriting input files: requested report and baseline destinations still use the existing atomic publication rules.

Machine-readable failures

Python failures include an analysis_errors array on the scanner summary and its failed_modules entry. Each error identifies the stage, exception_type, and full message. Its components array retains every failed source or scope, with file, symbol, reason, and start_line / end_line when known. These arrays are not truncated to the short human-readable examples. JSONL scanner and totals records preserve them; SARIF carries them in each failed module's notification properties.analysis_errors. Findings established before a limit and findings from independent Python functions and SQL source files remain available, while the scan stays incomplete and exits 2.

In --format=json|jsonl|sarif|toon mode every failure is an object on stdout, never just text on stderr, so ubs … --format=json | jq always has something to parse:

  • Environment errors and refused scans: {"error":"environment"|"refused","status":"error"|"refused","reason":"ast-grep-missing"|"module-failed"|"directory-too-large"|"home-directory"|"root-directory","exit_code":2,"message":…} (JSONL adds "type":"error"; SARIF emits one run with invocations[0].executionSuccessful=false and a toolExecutionNotifications entry).
  • Incomplete runs: the normal envelope with "status":"partial" (or "error" when a module could not run at all and no other scanner produced a result) and failed_modules:[{language,status,module_error,message}]; each scanner carries its own status (ok, timeout, error). A module that exits 2 is reported the same way, with "error":"environment", "reason":"module-failed" and "exit_code":2 added — the results from the scanners that did complete stay in the envelope, and every requested --report-json / --html-report / --beads-jsonl is still written. SARIF runs get invocations[].executionSuccessful=false, exitCode: 2 and one notification per failed module.
  • Requested artifacts are a separate outcome from the analysis. If a --report-json, --html-report, --save-baseline or --beads-jsonl was requested and could not be written, the run exits 2 and says which path failed; the previous file at that path is never truncated by a failed write.
  • Comparisons are qualified before they are shown. --comparison emits comparison.delta only when both the current scan and the baseline are complete; otherwise it emits comparison.status: "unavailable" with a reason (current_scan_incomplete, baseline_incomplete, baseline_missing, baseline_unreadable) instead of a number that would read as an improvement. --save-baseline refuses to record a baseline from an incomplete scan, leaving any existing baseline in place, and an HTML report from an incomplete scan carries a visible "Incomplete scan" banner.

Swift coverage limitations. The Swift analyzers can continue past a scoped limitation to retain findings in that scope and in other files. A swift.taint.analysis-limited or swift.lifecycle.analysis-limited diagnostic still makes the scan partial with exit 2. Warning thresholds and ordinary finding suppressions cannot turn that coverage failure into a successful receipt, and incomplete scans do not populate the reusable analysis cache. The separately documented, reason-required taint-source directive retains its narrow source-activation behavior; it does not clear another active source's analysis failure.

Directory size guard

UBS computes scan size after ignore filters (defaults + .ubsignore) over the files it will actually scan: binary, media, archive and data files (images, fonts, archives, databases, parquet/npy/pickle, csv/tsv/jsonl, …) and files above UBS_MAX_FILE_MB (default 8) are neither counted nor copied into the scan workspace, so a data directory next to the code does not make a small project "too large". It prints Scan size after ignores: XMB (limit YMB) before enforcing the limit. Override via UBS_MAX_DIR_SIZE_MB or UBS_SKIP_SIZE_CHECK=1, or pass --skip-size-check.

Cargo could not run (Rust). When cargo is present but a phase exits without compiler diagnostics (a build wrapper refusing, a missing toolchain component), the Rust module reports each phase as a warning plus "Not evaluated", surfaces the wrapper's message, and marks itself partial: the run's status becomes partial and the exit code 2 unless UBS_ALLOW_PARTIAL=1. A clean crate is never reported critical for a broken build environment.

Environment errors (exit 2)

If UBS prints an Environment error and exits 2, a required dependency is missing or unusable.

Most common fix for JS/TS projects:

ubs doctor --fix

Or install the dependency manually:

brew install ast-grep            # or: cargo install ast-grep, npm i -g @ast-grep/cli

If you’re intentionally scanning non-JS languages only, exclude JS:

ubs --exclude-langs=js .

Examples

# Basic scan
ubs .

# Verbose scan with full details
ubs -v /path/to/project

# Strict mode for CI (fail on any warning)
ubs --fail-on-warning --ci

# Save report without cluttering terminal
ubs . report.txt

# Scan Vue.js project
ubs . --include-ext=js,ts,vue

# Skip categories you don't care about
ubs . --skip=14  # Skip TODO/FIXME markers

# Maximum performance (use all cores)
ubs --jobs=0 .  # Auto-detect
ubs --jobs=16 .  # Explicit core count

# Skip languages you don't want in this run
ubs . --exclude-langs=ruby,swift

# Skip paths for this run only (same semantics as .ubsignore lines)
ubs . --exclude=legacy,generated

# Exclude extra paths (node_modules, vendor, dist, build are ignored by default)
printf 'legacy/\ngenerated/\n' >> .ubsignore

# Large directories (size guard)
UBS_MAX_DIR_SIZE_MB=5000 ubs .
UBS_SKIP_SIZE_CHECK=1 ubs .

# Custom rules directory
ubs . --rules=~/.config/ubs/custom-rules

# Combine multiple options
ubs -v --fail-on-warning --exclude=legacy --include-ext=js,ts,tsx . report.txt

JSONL schema

--format=jsonl (and --beads-jsonl=FILE) emit newline-delimited objects for easy piping into tools like Beads or jq:

{"type":"scanner","project":"/path/to/project","language":"python","files":42,"critical":1,"warning":3,"info":12,"timestamp":"2025-11-22T09:04:20Z"}
{"type":"totals","project":"/path/to/project","files":99,"critical":1,"warning":3,"info":27,"timestamp":"2025-11-22T09:04:22Z"}

Complete occurrence export

For editor diagnostics and other machine consumers, request the opt-in JSONL occurrence interface:

ubs src --ci --format=jsonl --all-occurrences > occurrences.jsonl
ubs src --ci --format=jsonl --all-occurrences --max-occurrences=10000
ubs src --ci --format=json --all-occurrences --also=jsonl:occurrences.jsonl
ubs --schema=jsonl

The flag requires a JSONL destination: --format=jsonl, --also=jsonl:FILE, or --beads-jsonl=FILE. It cannot be combined with --jsonl-summary-only. All requested renderings still come from one scan. The efficient sampled summaries remain the default, and the option does not change selected rules, severities, suppression, thresholds, or analyzer work limits. Python's separate --python-pattern-counts=FILE export remains unthresholded and independent.

Read the final totals record before accepting a complete diagnostic set. Every scanner record and the final totals record contain an occurrence_export object with schema: "ubs.occurrence-export/1". Only its complete: true promises that the selected analysis finished, counts reconcile exactly, and all counted occurrences were delivered with source locations and full available code. A missing final totals record, an error envelope, or absent completeness metadata never proves completeness.

Empty staged/diff selections retain exit 0 but now deliver a final totals record to every requested JSONL destination, with complete: false, analysis_complete: false, and reason no_files_scanned. A selection with no supported language retains the existing exit 3 policy and the same explicit no-coverage metadata. Neither case is presented as a completed clean analysis.

The stream contains these records in order:

Record Contract
type: "finding", schema: "ubs.occurrence/1" One active source occurrence, always count: 1. Includes occurrence_id, rule_id, language, severity, file, line, col, code, code_available, and code_truncated, plus the canonical message, fingerprint and analyzer evidence.
type: "rule_summary" One language/rule/severity group with reported, emitted, omitted, and unlocated. Here reported is the active weighted canonical ledger count for this rule; missing ledger records cannot be attributed to a rule from samples.
type: "scanner" Existing per-language counts and execution status, failure details when present, and occurrence coverage for that language.
type: "totals" Last record: existing combined counts, status and failed modules, with scan-wide occurrence coverage.

Locations are one-based and use the original source paths. code is the physical source line without its CR/LF terminator; indentation is preserved. Python code decoding honors its encoding cookie. A staged scan reads code from the analyzed index snapshot, even when the working file has different contents. An unavailable line is code: "", code_available: false; it is never replaced with a diagnostic message and presented as source text.

occurrence_id has the form sha256:<64 lowercase hexadecimal digits>. Version 1 hashes the language, rule, project-relative source path, line, column and canonical fingerprint. It is independent of timestamps, output limits and temporary scan directories, and is stable for repeated scans of the same unchanged input and options. Distinct counted occurrences remain distinct, including repeated text. IDs can change after source edits, line movement or changes to the selected analysis; they are not a cross-version semantic identity guarantee. The existing fingerprint retains its baseline semantics. Baseline filtering happens before export limits, and changing the export limit does not alter baseline contents.

Coverage fields distinguish the execution result from the delivered locations:

Field Meaning
analysis_complete All selected analysis completed according to the scanner/run status. A partial analyzer can retain useful findings while this is false.
locations_complete Reported counts and ledger counts reconcile, and every counted source occurrence was emitted with a usable location.
complete Both dimensions above are complete and every emitted code line is available and untruncated.
reported Existing summary counts, summed across critical, warning and info.
accounted Active canonical ledger occurrences, including weighted aggregates.
emitted Number of concrete occurrence records actually emitted.
omitted Undelivered count, computed per severity as max(reported, accounted) - emitted.
unlocated Accounted occurrences for which a producer has no concrete source site.
code_unavailable, code_truncated Emitted source occurrences whose code could not be read or exceeded the text limit.
truncated The record limit or code text limit omitted requested output.
reasons Explicit reasons for incomplete coverage, including analysis failures, count mismatches, missing locations, output limits or code extraction limits.
counts The same reported, accounted, emitted, omitted accounting separately for critical, warning, and info.

For example, a complete 50-warning ledger exported with a limit of 10 has reported: 50, accounted: 50, emitted: 10, omitted: 40, truncated: true, and complete: false. Its scanner still reports 50 warnings. A completed analyzer can instead have analysis_complete: true with locations_complete: false when a rule reports an aggregate without identifying each source site. UBS does not fabricate locations to make these counts match. Zero-count project notes and suppressed records do not become occurrences; report-only AST evidence does not inflate the counted ledger.

The exporter reuses the full finding sinks already produced by all shipped language modules. Summary previews stay capped. In occurrence mode, Rust's tech-debt/test-marker checks and Swift's package checks preserve their counted source sites before rendering. Rust's arithmetic spawn/handle and lifecycle heuristics, and Elixir's arithmetic net-count rules, remain explicitly unlocated aggregates. An enabled Rust type-narrowing analyzer failure marks occurrence-mode analysis partial and prevents reuse of that incomplete result from cache. Legacy or third-party producers with only sampled findings are reported incomplete when their counts cannot be accounted for; the exporter never falls back to summary samples and calls them complete.

Bounds and consumer checks

--max-occurrences accepts an integer from 1 through 1,000,000 and defaults to 100,000 across the entire scan, rather than per file or language. It limits exported source records after analysis and baseline filtering. The exporter still reads the ledger to report exact omitted counts. It streams the ledger rather than materializing all occurrences in memory. Per-rule and scanner summaries remain available after the source-record cap is reached. At most 10,000 distinct language/rule/severity groups are accepted; exceeding that metadata bound fails the export explicitly and preserves its previous file.

Each code field is limited to 4,096 decoded characters. A longer line sets code_truncated: true and makes the export incomplete. Code extraction keeps eight source readers, caches at most 1,024 requested lines, and has a separate 256 Mi-character read-work budget. Python encoding detection reads at most 64 KiB from each of the first two header lines. A read/decode failure or code work limit sets code_available: false and an explicit reason. These bounds do not change analyzer counts or relax any existing analysis budget.

Finding-based exit codes remain unchanged by output caps. Analyzer failures still retain the existing partial-run exit policy; report delivery failures exit 2 and preserve a previous destination file. Consumers requiring a complete diagnostic set must also check the last record, for example:

jq -e -s '.[-1] | .type == "totals" and .occurrence_export.complete == true' occurrences.jsonl
jq -c 'select(.type == "finding" and .schema == "ubs.occurrence/1") |
       {occurrence_id, rule_id, severity, file, line, col, code}' occurrences.jsonl

Completeness is scoped to the selected inputs, rules and enabled analyses; it does not claim coverage of excluded files, skipped categories or all possible bugs. Cross-file thresholds remain selection-dependent, so combining separately scanned subsets is not generally equivalent to scanning their union.

Python dependency vulnerability audits

When optional Python analyzers are enabled, pip-audit selects the first available dependency inventory in this order:

  1. requirements.txt, resolved by pip-audit, including transitive dependencies and any requirements/constraints files it references.
  2. PEP 751 pylock.toml and pylock.*.toml files, audited with pip-audit --locked PROJECT_DIR. All listed package versions in all of these lockfiles are checked; the audit does not filter lockfile markers or validate that a lockfile is current with the project manifest.
  3. Static runtime dependencies declared in [project] in pyproject.toml, resolved by pip-audit's positional project-directory interface.

A manifest is never treated as an installed-package directory, and the tool's own installed environment is never used as the project's inventory. The audit runs for text, JSON and SARIF reports and contributes py.pip-audit.<advisory>.<package> critical findings, including the package version, available fixes and advisory aliases. Final counts and exit status include these findings. Package names are normalized in the finding identity so baselines remain stable across spelling aliases and cannot suppress a different package affected by the same advisory.

The Python summary's extras.external_tools records the audit input, status, dependency and vulnerability counts, and skipped packages. For a lockfile audit, inputs lists every selected lockfile; findings use the generic lockfile, or the first sorted named lockfile, as their project-level location. A missing manifest or unavailable tool is explicit skipped coverage. Dynamic dependencies, legacy project metadata, and unsupported lockfiles (uv.lock, poetry.lock, Pipfile.lock) require an exported resolved requirements.txt or PEP 751 lockfile. An unsupported lockfile is never silently replaced with fresh resolution of potentially different versions from pyproject.toml.

These unsupported inputs, failed resolution, skipped packages, invalid reports, timeouts, and any attempted audit with zero audited packages make the scan incomplete (exit 2), retaining any valid advisories already received. A completed nonempty audit with vulnerabilities contributes critical findings (exit 1). An explicitly declared empty runtime-dependency list is skipped without running the tool; that skipped coverage is not a vulnerability-free audit result. Audits run afresh even when the source-analysis cache hits.

To select only the dependency auditor through the runner:

UV_TOOLS=pip-audit ubs . --only=python --format=json

The direct module also accepts --uv-tools=pip-audit. Use ENABLE_UV_TOOLS=0 with the runner, or --no-uv with the direct module, to disable these optional analyzers.

The optional-analyzer regression suite runs without advisory-service access by default. With pip-audit installed, enable the real resolver and advisory-service regressions explicitly:

UBS_TEST_PIP_AUDIT="$(command -v pip-audit)" \
  python3 test-suite/shareable/test_optional_analyzers.py RealPipAuditIntegration -v

These live tests check a known vulnerable package through requirements, project, and PEP 751 inputs, plus empty inventories and failed dependency resolution.

Python pattern count export

Python pattern findings use distinct matching lines across the selected input to choose a rule's severity. A complete scan can therefore have matching lines without reporting a finding. For example, py.functions.high-param-count and py.regex.nested-quantifiers each require more than three matching lines to report warnings. This is an occurrence threshold, not a threshold on the number of files. One or three matching lines produce no finding for that rule; four produce a warning count of four. Empty-file padding does not change those counts. Other rules have different thresholds or additional context conditions.

Request a separate, unthresholded count report when consuming these measurements:

ubs src --only=python --ci --format=json \
    --python-pattern-counts=pattern-counts.json
ubs --schema=python-pattern-counts

# The direct Python module supports the same sidecar.
modules/ubs-python.sh src --ci --no-uv --python-pattern-counts=pattern-counts.json

# Nonzero counts, including rules with no exported finding:
jq '.counts[] | [.file, .rule_id, .count]' pattern-counts.json
# The rule's selected-input total, thresholds and effective finding severity:
jq '.rules[] | select(.rule_id == "py.functions.high-param-count")' pattern-counts.json

The option works alongside text, JSON, JSONL, SARIF and TOON output from ubs. It does not change ordinary findings, warning totals, the three-sample JSON preview limit, or severity-based exit policy. Report delivery errors still exit 2, as with other requested exports. Relative destinations are relative to the invocation directory, and report destinations must be distinct. Python must be among the selected scanners. If no Python scanner runs, or a module cannot produce the requested export, the request exits 2 and leaves any previous destination unchanged.

The JSON document uses schema: "ubs.python-pattern-counts/1" and contains:

Field Contract
files Sorted selected Python-module input paths as { "file": "…" }, including inputs with zero pattern matches. Selection, exclusions and category skipping still apply.
counts Sparse { "file": "…", "rule_id": "…", "count": N } records for nonzero local counts, sorted by file and rule. Counts are not truncated by the sample limit.
rules Every loaded, active Python pattern rule, including rules with zero matches. Each entry has its rule/category IDs, title, selected-input count, threshold ladder, threshold scope, resolved severities and context decision.
skipped_categories Category numbers excluded from this run; their rules are not represented as evaluated zeroes.
status, complete, errors ok / true / [] only after the Python analysis completes. Available evidence from an incomplete Python analysis is partial / false with diagnostics. This is conservative: a failure in another Python analysis layer also prevents a complete count report.
count_unit, count_stage, zero_counts Explicit markers for distinct match-start lines, counting after source suppression and before context/thresholds, and the sparse-zero rule described below.
excluded_layers Detectors, registry analyzers, ast-grep (including custom AST rules) and external tools are outside this export. It is a Python pattern-rule contract, not an inventory of every Python finding.

Counting and zeroes. A count is the number of distinct physical source lines on which a rule's matches start, once per (file, rule_id, line). Two matches on one line count once; a multiline match counts its start line, not every line it spans. Existing rule-specific source views, exclusions and post-filters remain in effect: most rules blank strings, some inspect strings, and code-only rules also blank comments. The export reuses the same retained source-local match facts on cold and warm caches.

An absent counts record means zero only when complete is true and both the file and rule are listed in files and rules. An excluded file, skipped rule, unsupported analysis layer or incomplete run must not be inferred to have zero matches. complete covers the selected input; it does not claim coverage of unselected repository files. A missing sidecar after an error is not a zero-count report, and a previous report at that destination must not be mistaken for the current run.

Suppression. “Unthresholded” does not mean “unsuppressed.” Python's shared source suppression index removes matches before raw counts and thresholds are computed. Bare # ubs:ignore covers the applicable statement; scoped markers such as # ubs:ignore[py.functions.high-param-count] affect only the named rule. A ubs:ignore[taint-source] <reason> marker is different: on a source line it drops only that line's taint source(s), so a function whose only activator was that source is no longer analyzed. The [taint-source] id and a non-empty reason are both required. A bare ubs:ignore or any other id is not a source directive. A multi-id list containing taint-source, or an empty reason, does not drop and is reported as ignored. Every drop is listed (file:line, reason) in the summary and the JSON output, and a scan with drops is still complete. It is activation-only and can never clear a refusal: a function still activated by another, unannotated source is analyzed and reports or refuses exactly as before. Trailing, immediately preceding, multiline-statement and supported formatter-relocated markers use the same statement-interval semantics as normal findings. A marker inside a string is not a suppression. Suppressed matches are not exported or counted, including during cache replay.

Thresholds and context. rules[].thresholds is an ordered ladder of { "min_count_exclusive": N, "severity": "warning" } entries. The first entry for which the selected-input count is greater than N wins. threshold_severity records that result or null when no threshold is met. reported_severity is the same severity only when the rule's context is also eligible; otherwise it is null. These are selected-input decisions, not a severity independently recalculated from each file's count. On incomplete runs they describe the available evidence and are not final completeness claims.

context.gate and context.suppress_when are either null or objects containing the regex, its Python regex flags and whether it matched. These conditions use raw text across all selected files, including comments, literals and files with no candidate match. A required gate must match somewhere; a suppressor matching anywhere makes the rule ineligible. Source suppression of a finding does not remove text from this context evidence. Raw counts remain visible when such a condition prevents ordinary findings. For example, the existing py.async.un-awaited-paths suppressor can be satisfied by await text in a different selected file.

Additivity boundary. For complete runs of the same unchanged files, rule definitions and local filtering options, these source-local counts can be summed over disjoint file selections. Ordinary finding counts and severities remain scope-dependent because of thresholds and context gates. This is not a promise of additivity for cross-file detectors, taint/data-flow analyses, other languages, or arbitrary source concatenation: concatenation can change syntax, string/comment boundaries, statement suppression and file-role exclusions.

JavaScript prototype pollution

js.security.prototype-pollution follows request-derived merge inputs and computed property keys through local bindings, destructuring, reassignment, branches and loops. Findings point to the actual merge or property write. Names such as safeMerge or validatePrototypeKeys do not establish safety, and validation of an unrelated value cannot suppress a finding.

A null-prototype exemption belongs to the actual Object.create(null) allocation and its aliases. It covers shallow writes and Object.assign, not arbitrary nested traversal or deep merges. Mutation or unresolved calls revoke receiver guarantees. Request-derived keys stored in such a bag remain dangerous when later copied into an ordinary object; fixed ordinary keys do not become prototype keys merely because their values came from a request.

A selected local key validator must reject non-string inputs and all of __proto__, constructor and prototype, then return the same parameter. The rejection must dominate the write, and rebinding or shadowing revokes the proof. Arbitrary sanitizers and general interprocedural mutation effects remain outside this local model. Existing rule-specific statement suppressions apply at the sink; suppressing a source-line diagnostic does not clear its taint.

Python subprocess taint

python.taint.command distinguishes executable selection, shell command text, and source interpreted by programs such as sh and python. With shell=False, an ordinary argument to a fixed executable is not executable taint. Interpreter operands such as python -c SOURCE still execute source, even without shell=True.

Known list/tuple vectors and *argv packs retain separate operand facts through local wrapper summaries, including nested calls. Program/option alternatives stay paired with their own arguments. To bound recursive analysis, summaries retain a finite positional prefix and conservatively combine the remaining suffix; mutation or an opaque operation revokes the known shape. This keeps uncertain data tainted rather than silently declaring it safe.

Indexing and exact slicing use the evaluated vector, including positions after * expansion. A lookup observes mutations performed while evaluating its index. Copies retain references to mutable elements across wrapper calls. Copies of known immutable arguments do not acquire later writes to the original outer list.

If the executable's identity cannot be resolved, UBS conservatively treats it as a possible interpreter. A value with no recognized taint source can still name an interpreter, so that alone does not prove its arguments are ordinary data. Resolved literal executables retain the positional distinction above.

For a known process created with stdin=PIPE, the analyzer carries its stdin execution mode through helper parameters, returned process handles, and saved communicate methods. Python source remains distinct from shell text: shell quoting cannot sanitize Python or an unresolved interpreter's input. Known data-only modes, such as python script.py or a fixed python -c program, do not treat subsequent stdin data as source. An unrelated object's communicate method does not establish a subprocess sink.

Returned local functions and lambdas retain their defining lexical cells. Calling a closure reads the current captured values; an unrelated variable with the same name in its caller does not supply those values. Sibling closures share nonlocal assignments and deletion, including writes on exceptional exits and in finally. Captured vectors and process handles keep their argument positions and stdin contracts through these calls. Awaited local coroutines also read live captured cells when resumed; ordinary arguments and evaluated defaults keep their existing snapshot semantics.

Closure summaries separate cell identity from concrete taint provenance. Allocation contexts are bounded, so repeated or deeper factory instances may be conservatively joined. This bound does not discard their taint or turn an unresolved executable into a known data-only program.

Generators with live lexical cells still require a suspension/resumption model. A narrow supported case is a proven stable local list used only as an ignored-result append, extend, or insert receiver: its weak heap writes remain conservative. Other live-cell generators report incomplete analysis and retain already established findings. The existing heap model also does not infer arbitrary alias relationships between separate helper parameters.

Diagnostics remain at the subprocess call in the wrapper, with at most one python.taint.command finding per process call. Executable, shell, and interpreter-source facts are checked separately after substituting each caller's inputs, so argument-only callers do not contribute executable evidence for a resolved vector.

Environment values and captured output. Process results conservatively retain taint from supplied arguments, input=, and env=. This includes subprocess.run(...).stdout, subprocess.check_output(...), and output read through Popen(...).communicate(). Python passes an explicit env mapping to the child process, which can read those values and incorporate them into its output (Python subprocess documentation). Passing a tainted environment value alone does not produce a command-injection finding; a finding requires a subsequent flow into an unsafe sink.

For example, the environment-only case in issue #200 retains this dependency:

import os
import subprocess

env = {"PATH": "/usr/bin", "SSH_CONNECTION": os.environ.get("SSH_CONNECTION", "")}
wan = subprocess.run(["ip", "-j", "route"], capture_output=True, text=True, env=env).stdout.strip()
subprocess.run([wan], check=False)  # Output is used as an executable name.

UBS reports the final call because its executable depends on a process result with tainted inputs. The analysis does not model the internals of ip or prove that it reads SSH_CONNECTION; this is a conservative dependency and can need review for a particular program. It is separate from treating a wrapper's ordinary arguments as executable names. Using wan as an ordinary argument to a fixed, non-interpreting executable does not trigger this taint rule.

The process result is currently represented by one aggregate taint fact, not separate facts for every CompletedProcess field. A clean result from a call with no recognized taint source is therefore not proof that arbitrary child output is trustworthy. Keep executable names fixed and validate child output according to its intended use; shell quoting does not authorize it as an executable or sanitize interpreter source.

Custom AST-Grep Rules

You can add your own bug detection patterns:

# Create custom rules directory
mkdir -p ~/.config/ubs/rules

# Add a custom rule (YAML format)
cat > ~/.config/ubs/rules/no-console-in-prod.yml <<'EOF'
id: custom.no-console-in-prod
language: javascript
rule:
  any:
    - pattern: console.log($$$)
    - pattern: console.debug($$$)
    - pattern: console.info($$$)
severity: warning
message: "console statements should be removed before production"
note: "Use a proper logging library or remove debug statements"
EOF

# Run with custom rules
ubs . --rules=~/.config/ubs/rules

Common custom rules:

# Enforce specific naming conventions
id: custom.component-naming
language: typescript
rule:
  pattern: export function $NAME() { $$$ }
  not:
    pattern: export function $UPPER() { $$$ }
severity: info
message: "React components should start with uppercase letter"
# Catch specific anti-patterns in your codebase
id: custom.no-direct-state-mutation
language: typescript
rule:
  pattern: this.state.$FIELD = $VALUE
severity: error
message: "Never mutate state directly - use setState()"

severity takes ast-grep's levels (error, warning, info, hint); UBS reports error as critical. critical is not an ast-grep level, so ast-grep refuses to parse the rule and the scan ends as partial (exit 2). Rules may sit in subdirectories of the --rules directory, as .yml or .yaml; hidden directories such as .github/ are skipped. Every language module runs your rules as their own layer: each finding keeps its rule id and the severity you gave it.

Several rule sets, one policy. --rules is repeatable, so a team-wide rule set and a project's own rules can run together. When no --rules is given, UBS_RULES supplies the directories, separated by : like PATH, so a pre-commit hook, CI and a manual ubs <changed files> all enforce the same rules without a wrapper script:

export UBS_RULES="$HOME/.config/ubs/team-rules:$PWD/.ubs/rules"
ubs .                                              # both rule sets
ubs --rules=./experimental .                       # --rules replaces UBS_RULES for this run

A rule id defined twice, in two files or twice in one file, refuses the scan (duplicate-rule-id, exit 2) and names the files, rather than letting one rule silently shadow the other. A file reached twice (overlapping directories, a symlink) is one rule.

A directory holding an ast-grep project (sgconfig.yml or sgconfig.yaml) contributes only the directories its ruleDirs lists, so a rules repository can keep its rule-tests/ and still run under ast-grep test. Utility rules (utilDirs) are not loaded; a rule that matches one fails to parse and the scan ends as partial.

Rules are read only from local directories. To share rules across repositories, keep them in a repository of their own and check it out (a git submodule, a pinned clone in CI); UBS does not fetch rule packs at scan time, since that would make every scan depend on the network and on code you have not reviewed.

Repository Scan Configuration

Commit a .ubs.json file to keep local, CI, and service scans on the same policy:

{
  "version": 1,
  "only": ["js", "python"],
  "exclude": ["/generated/**", "*.min.js"],
  "profile": "strict",
  "skip": ["python.todo"],
  "skip_by_lang": {"js": [11]}
}

The supported fields are only, exclude_langs, exclude, profile, skip, skip_by_lang, category, fail_on_warning, and rules. Language lists accept the same aliases as the CLI. skip accepts category numbers or stable language.category ids; skip_by_lang maps each language to category numbers. category currently accepts only resource-lifecycle. rules is an array of custom rule directories, and fail_on_warning is a JSON boolean. The optional version field must be the integer 1.

Precedence is defaults, project configuration, documented environment variables, then explicit CLI options. Each field replaces the corresponding lower layer; each normalized language in skip_by_lang is replaced separately. Empty arrays clear that field. Built-in ignores and .ubsignore remain independent, and repeatable CLI options keep their existing behavior. UBS_SKIP_CATEGORIES, UBS_RULES, and the strict/loose values of UBS_PROFILE override their project fields. Numeric UBS_PROFILE=1 enables timing output alongside the effective severity profile; collecting timings cannot turn a strict warning gate into a passing scan.

ubs .                                  # use project .ubs.json when present
ubs . --only=python --skip=             # replace two project fields for this run
ubs --config=./ci.ubs.json .            # use a different JSON policy
ubs --no-config .                      # use CLI/environment/defaults
UBS_PROFILE=1 ubs . --profile=strict    # retain both strict gating and timings
ubs --schema=config                   # machine-readable schema
ubs robot-docs config                 # discovery, precedence and bounds

For a single directory or positional file, discovery uses its nearest Git worktree root; outside Git, it uses the selected directory or file's parent. For multiple paths or --files, discovery uses the invocation directory's Git root, or that directory outside Git. Only that root's .ubs.json is loaded: there is no merging with parent or nested configuration files. --config paths are relative to the invocation directory and conflict with --no-config.

Exclusion globs remain anchored to the discovered project, including when a scan selects only a subdirectory. Relative rules directories are anchored to the configuration file's parent. Configuration never expands the selected scan targets; explicitly named files retain their existing override of ignores. Staged and diff scans retain their selected directory scope and source snapshot.

Invalid configuration fails with exit 2 before a scan starts or report outputs are opened. Unknown or duplicate keys, unsupported category ids, incorrect types, unreadable files, and invalid rule directories are errors. Documents are limited to 256 KiB, arrays to 256 entries, and strings to 4096 characters. Control characters are rejected, and fields translated to CSV cannot contain commas. Configuration is read as JSON data and is never sourced or evaluated as shell code.

Configuration identity and effective settings participate in the native scan cache. The local service observes configuration changes. Selected-file watches automatically observe creation, edits, and removal of the .ubs.json selected by normal CLI discovery, including a change in its location after Git is initialized. If a selected path inherits policy from outside the served directory, start the service and watch with --repo set to its Git worktree root. Whole reports are not reused while custom rules are active, because rule directories can reference policy outside the service snapshot; the native incremental cache remains available. File watching stays within the served repository: use an explicit watch dependency for in-repository rule files, and request a fresh scan after changing external rules.

Excluding False Positives

If the scanner reports false positives for your specific use case:

# Skip entire categories by stable id or number
ubs . --skip=js.debug,python.todo  # Skip debug code detection and TODO markers

# Exclude specific files/directories (gitignore-style globs, one per line)
printf 'legacy/\nthird-party/\ngenerated/\n' >> .ubsignore

# Put shared category/language/rule settings in .ubs.json (see above).
# Use --no-config for a one-off scan without the repository policy.

🎓 How It Works (Under the Hood)

Multi-Layer Analysis Engine

The scanner uses a sophisticated 4-layer approach:

Layer 1: PATTERN MATCHING (Fast) ──┐
├─ Regex-based detection           │
├─ Optimized with ripgrep          │
└─ Finds 70% of bugs in <1 second  │
                                    ├──► Combined Results
Layer 2: AST ANALYSIS (Deep) ──────┤
├─ Semantic code understanding      │
├─ Powered by ast-grep             │
└─ Catches complex patterns        │
                                    │
Layer 3: CONTEXT AWARENESS (Smart) ┤
├─ Understands surrounding code     │
├─ Reduces false positives         │
└─ Knows when rules don't apply    │
                                    │
Layer 4: STATISTICAL (Insightful)  │
├─ Code smell detection            │
├─ Anomaly identification          │
└─ Architectural suggestions       │
                                    ↓
                         Final Report (3-5 sec)

Technology Stack

Component Technology Purpose Why This Choice
Core Engine Bash 4.0+ Orchestration Universal compatibility, zero dependencies
Pattern Matching Ripgrep Text search 10-100x faster than grep, parallelized
AST Parser ast-grep Semantic analysis Understands code structure, not just text
Fallback GNU grep Text search Works on any Unix-like system
Rule Engine YAML Pattern definitions Human-readable, easy to extend

AST Rule Architecture: Ancestor-Aware Pattern Matching

UBS's ast-grep rules use a sophisticated technique called ancestor traversal to drastically reduce false positives. The key directive stopBy: end ensures patterns check the entire ancestor chain rather than just the immediate parent.

The Problem Without Ancestor Traversal:

// This code is SAFE - the fetch is properly handled:
async function safeFetch() {
  try {
    fetch('/api');  // Inside try block - exception will be caught
  } catch (e) {
    handleError(e);
  }
}

// Naive AST rule checking only immediate parent:
// ❌ False positive! Reports "fetch without catch" because
//    fetch()'s immediate parent is the ExpressionStatement,
//    not the try block.

The Solution - Ancestor Traversal with stopBy: end:

# ast-grep rule with proper ancestor checking
rule:
  all:
    - pattern: fetch($ARGS)
    - not:
        inside:
          kind: try_statement
          stopBy: end           # ← Key directive: traverse ALL ancestors
    - not:
        inside:
          pattern: $_.catch($$)  # Check for .catch() in chain
          stopBy: end

The stopBy: end directive instructs ast-grep to walk up the entire ancestor tree until it finds a match (or reaches the root). Without it, only the immediate parent is checked—missing try blocks, function boundaries, and method chains.

Real-World Impact:

Scenario Without stopBy: end With stopBy: end
try { fetch() } catch {} ❌ False positive ✅ Correctly ignored
fetch().then().catch() ❌ False positive ✅ Correctly ignored
return fetch() ❌ False positive ✅ Correctly ignored
fetch() standalone ✅ Detected ✅ Detected

This technique is applied across 19+ rules in the JavaScript module alone, covering:

  • Promise chain detection (.then(), .catch(), .finally())
  • Try-catch context awareness
  • Return statement handling
  • Async/await scope analysis

Inline Suppression Comments

When a finding is intentional or a known false positive, suppress it inline:

// Suppress a single line:
eval(trustedCode);  // ubs:ignore

// Suppress with reason (recommended):
eval(adminScript);  // ubs:ignore -- admin-only trusted input
# Python suppression:
exec(validated_code)  # ubs:ignore

# Ruby suppression:
eval(safe_string)  # ubs:ignore

Suppression Rules:

  • A marker trailing the flagged line always suppresses (most robust placement)
  • A marker on the line immediately above the flagged line also suppresses:
    // ubs:ignore -- trusted admin input
    eval(code);  // suppressed
    
  • For a multi-line statement, a marker on any physical line of the statement — or on the line immediately above the statement's first line — suppresses a finding reported against any of its continuation lines
  • If a formatter relocates a trailing marker off a block-opening line (if (...) { // ubs:ignore becoming a comment on the first line inside the block), the marker still suppresses the finding on the opening line
  • The trailing-line and previous-line placements work across all 10 supported languages; the multi-line-statement and formatter-relocated placements are implemented in the JavaScript/TypeScript module today (bead ultimate_bug_scanner-0xjg.16 brings them to every module)
  • Suppresses all findings on the covered lines (use sparingly)

Anti-patterns to avoid:

// ❌ Wrong - don't blanket-suppress large blocks:
/* ubs:ignore */  // A standalone marker only covers the next line, not a block

Cross-Language Async Error Detection

UBS detects selected unhandled async and failure-observation patterns in the languages below. Coverage follows each language's supported APIs and idioms; the PHP module currently focuses on request-flow security:

Language Pattern What UBS Detects
JavaScript/TypeScript promise.then() without .catch(), new Promise(async ...), forEach(async ...), async array predicates, async timer/event/JSX-handler callbacks, Promise.all(map(...)) without callback return Dangling promises, missing await, unawaitable async callbacks, unhandled rejections
Python asyncio.create_task() without await Orphaned tasks, missing await, unclosed coroutines
Go Goroutine without error channel Fire-and-forget goroutines, leaked contexts
Rust .unwrap() / .expect() after partial guard Panic after if let Some, missing ? operator
Java CompletableFuture without .exceptionally() Swallowed exceptions, missing join()
Ruby Thread.new without .join Zombie threads, unhandled thread exceptions
C++ std::async without .get() Ignored futures, exception propagation
Swift Task {} without error handling Unstructured concurrency leaks
C# Task.Wait() / .Result / throw ex; Sync-over-async deadlocks, stack-trace loss, unsafe exception surfaces

JavaScript Promise Chain Analysis:

The scanner understands complex promise chains:

// ✅ Handled - .catch() at end of chain:
fetch('/api')
  .then(r => r.json())
  .then(data => process(data))
  .catch(handleError);  // Scanner recognizes this catches all above

// ✅ Handled - .catch() before .then():
fetch('/api')
  .catch(e => fallback)  // Early catch
  .then(r => r.json());

// ❌ Unhandled - .finally() doesn't catch:
fetch('/api')
  .then(r => r.json())
  .finally(cleanup);  // Flagged: finally doesn't handle rejections

// ❌ Unhandled - no error handling:
async function leaky() {
  fetch('/api');  // Flagged: fire-and-forget promise
}

Helper Script Verification

Language-specific helper scripts (Python AST walkers, Go analyzers, TypeScript type checkers) are verified with SHA-256 checksums before execution:

# Helper checksums embedded in each module:
modules/helpers/
├── async_task_handles_csharp.py   # SHA-256 verified
├── resource_lifecycle_csharp.py # SHA-256 verified
├── resource_lifecycle_py.py    # SHA-256 verified
├── resource_lifecycle_go.go    # SHA-256 verified
├── resource_lifecycle_java.py  # SHA-256 verified
├── type_narrowing_csharp.py    # SHA-256 verified
├── type_narrowing_ts.js        # SHA-256 verified
├── type_narrowing_rust.py      # SHA-256 verified
├── type_narrowing_kotlin.py    # SHA-256 verified
└── type_narrowing_swift.py     # SHA-256 verified

The ubs doctor command validates all helper checksums:

$ ubs doctor
🏥 UBS Environment Audit
────────────────────────
✓ helper checksum verified (resource_lifecycle_py.py)
✓ helper checksum verified (type_narrowing_ts.js)
...

If a helper is modified or corrupted, the scanner fails safely with remediation guidance rather than executing unverified code.

Unified Severity Normalization

All 14 language modules normalize their findings to a consistent severity scale, ensuring predictable output regardless of source language:

┌─────────────────────────────────────────────────────────────┐
│  Language Tool Output      →   UBS Normalized Severity      │
├─────────────────────────────────────────────────────────────┤
│  ESLint "error"            →   critical                     │
│  Pylint "E" / "F"          →   critical                     │
│  Clippy "deny"             →   critical                     │
│  Go vet "error"            →   critical                     │
│  SpotBugs "High"           →   critical                     │
│  RuboCop "Fatal/Error"     →   critical                     │
├─────────────────────────────────────────────────────────────┤
│  ESLint "warn"             →   warning                      │
│  Pylint "W" / "R"          →   warning                      │
│  Clippy "warn"             →   warning                      │
│  Go vet "warning"          →   warning                      │
│  SpotBugs "Medium"         →   warning                      │
│  RuboCop "Warning"         →   warning                      │
├─────────────────────────────────────────────────────────────┤
│  ESLint "suggestion"       →   info                         │
│  Pylint "C" / "I"          →   info                         │
│  Clippy "note"             →   info                         │
│  SpotBugs "Low"            →   info                         │
│  RuboCop "Convention"      →   info                         │
└─────────────────────────────────────────────────────────────┘

Benefits of normalization:

  • Consistent exit codes: Exit 1 always means "critical issues found" across all languages
  • Unified JSON/SARIF output: Downstream tools parse one schema, not 8 different formats
  • Predictable --fail-on-warning: Same behavior whether scanning Python, Rust, or TypeScript
  • Cross-language metrics: Compare code quality across polyglot projects fairly

Each module maps its tool-specific severity strings, numeric levels, and legacy format variations onto this scale before reporting (the Swift module does it through a dedicated normalize_severity(); the shared implementation for every module lives in the module-library work, bead ultimate_bug_scanner-0xjg.1).

Performance Optimizations

# Automatic parallelization (uses all CPU cores)
- Auto-detects: 16-core = 16 parallel jobs
- Manually set: --jobs=N

# Smart file filtering (only scans relevant files)
- JS/TS: .js, .jsx, .ts, .tsx, .mjs, .cjs (auto-skip node_modules/dist/build)
- Python: .py + pyproject/requirements (skip venv/__pycache__)
- C/C++: .c/.cc/.cpp/.cxx + headers + CMake files (skip build/out)
- Rust: .rs + Cargo manifests (skip target/.cargo)
- Go: .go + go.mod/go.sum/go.work (skip vendor/bin)
- Java: .java + pom.xml + Gradle scripts (skip target/build/out)
- Ruby: .rb + Gemfile/Gemspec/Rakefile (skip vendor/bundle,tmp)
- Custom: --include-ext=js,ts,vue

# Working files and memory
- Whole-project scans create no temporary copy of the tree: the meta-runner
  lists the files to scan (ignores applied; binary/data files and files above
  UBS_MAX_FILE_MB left out) and feeds per-language file lists directly to the
  modules via `--files-from` against the source tree
- One explicit source file (the agent-hook case, `ubs FILE --ci`) is handed to
  its language module in place: no workspace, no per-language detection walk
- Explicit multi-file targets, directory targets, and git modes (--staged/--diff)
  prepare a workspace copy using copy_file_list (rsync with tar/python fallback,
  so Git for Windows works)
- Results are merged from per-module JSON at the end of the run
- Memory: 45–110 MB resident on typical projects; ~840 MB was measured on a
  407K-line TypeScript tree (bead ultimate_bug_scanner-q150.6 streams that)

# Incremental scanning
- Scan only changed files today: ubs --staged / ubs --diff / ubs <files>
- Content-hash cache of previous results: bead ultimate_bug_scanner-q150.4

Python call-chain analysis and work limits

Python security category 7 reuses function summaries. When selected function bodies contain no nested function, lambda, or class definitions, it schedules changed callees before their callers, including newly discovered callback contexts. Long subprocess chains on this path no longer require a whole-file pass for each link. Recursive calls still converge through summary dependencies; there is no call-depth cutoff.

Selected modules share summary and definition metadata only within connected import groups. The groups include callbacks in both directions and namespace descendants exposed by imports, so package attributes and deferred imports keep their cross-file flows. Unrelated files no longer scan each other's summary tables. This grouping does not change module initialization order or which import cycles need conservative analysis.

If any function owner in a group can define nested callables or classes, UBS retains the original source-order passes within that group. A reader of a captured default must stay between the writes that surround it; processing all writers first can hide a tainted value already returned by an earlier callback. This conservative path also covers imported helpers and can offer less scheduling speedup. Sink-specific sanitizers, mutation effects, work budgets, and incomplete-result handling remain active on both paths.

Source-order passes reuse the module's initialization transfer when its namespace and observed summary/definition inputs are unchanged. Replaying declaration writes preserves their original position in the pass. Changed inputs invalidate the reuse, including changed evidence witnesses; active import cycles keep the uncached path. A nested definition therefore no longer forces every settling call-chain link to execute unrelated module statements.

Simple declaration/return factories also exclude unrelated globals from their summary contexts and captured-value projection. A cached syntax proof permits only direct lexical reads and checks nested definitions' executable headers; calls, decorators, class bodies, imports, control flow, and protocol-dispatch expressions keep the broader analysis. Relevant lexical and deferred-child reads, explicit global overrides, and observed module-qualified capture dependencies remain active. A growing module namespace therefore does not create a new factory context for every unrelated assignment. Regressions with 10, 50, and 200 factory calls check exact unsafe, clean, and mixed-instance findings under the unchanged budgets.

A paired 200-factory/200-caller fixture previously exhausted the factory's 20,000-unit allowance after 422,409 total units. The restricted dependency selection completes with the same 200 exact findings in 6,646 total units and two factory solves. The comparison uses identical source and work limits; clean controls retain no taint findings.

Completed imported base summaries are reused across callers; new callback contexts and active import cycles still receive their required analysis. The worklist retains dependency metadata and the latest effects while releasing completed functions' temporary expression, heap, and completion states.

Functions and lambdas returned or yielded by a factory retain the facts, object references, and callable identities of their evaluated defaults. Creating another callback cannot replace an earlier callback's captured defaults. Explicit call arguments override those defaults, while later writes through a captured mutable object remain visible. Shared body summaries use symbolic default inputs, so different concrete values do not create a new summary context for every factory invocation. This also applies to awaited factories and generator completion values. Incomplete factories retain observed captured sources while revoking unfinished sanitizer guarantees.

Summary substitution joins expanded facts once, avoiding repeated copies of a growing result. A 1,024-entry cache reuses immutable witness transformations; its key includes source, parameter, sanitizer proofs, the complete evidence path, appended steps, and the path limit. It retains no analysis or AST state, and cache hits still consume the same analysis work budget.

Within each call, a separate cache retains up to 256 parameter-trace expansions shared by downstream sink, mutation, exception, and returned-value summaries. It distinguishes complete formal witnesses and immutable actual argument facts, including their evidence paths. Once full, it computes additional expansions normally; the capacity never limits coverage. The cache is released after that call's summary projection, and response-header proofs retain their own revocation semantics. Cached expansions consume the original work budget.

Taint and response-header analysis honor Python encoding cookies and UTF-8 byte-order marks. Unreadable, undecodable, or invalid selected sources report incomplete coverage instead of disappearing from a successful scan. Input errors are counted with bounded diagnostics, and findings from independent valid inputs are emitted before the incomplete result.

The generic ubs_core taint and ubs_core scan commands also keep the full selected Python project together when --jobs is requested. Splitting an import across file shards can hide a real flow. Failed analyzers retain their completed finding records and allow independent analyzers or shards to finish; the helper CLI then reports the failures on stderr and exits 2.

SQL analysis first decodes and parses every selected Python source. If the whole syntax tree contains no call from the detector's modeled SQL sink families, it skips the SQL dataflow pass. Nested bodies and definition-time calls are included in this check; matching calls retain full analysis even when their arguments are unknown. Read, decode, parse, and applicable analysis failures continue to report incomplete coverage.

Within supported effectful any() and all() generators, SQL analysis follows comparison chains such as 0 <= column <= 10 using proven builtin value shapes. Middle operands execute once; later operands run only on continuing paths. Literal comparisons can prove that a later SQL call is unreachable, while an unknown comparison result retains both mutation alternatives. Exceptions keep the SQL effects already observed, and a nonreturning operand cannot erase an earlier false path. Arbitrary rich-comparison, membership and truth callbacks still report incomplete analysis rather than receiving builtin guarantees.

Taint and response-header analysis use the same bounded work policy. They run separately and each has its own counters; the header detector has no additional fixed 250,000-expression cutoff. Transfers, summary scheduling, and fact substitution consume work units rather than elapsed seconds.

Environment variable Work boundary Default when unset
UBS_PYTHON_FLOW_MAX_STEPS Total work for one selected source, in each analyzer min(10000000, max(500000, 128 * AST node count))
UBS_PYTHON_FLOW_SCOPE_STEPS Cumulative work for one function across passes and callback contexts min(250000, max(20000, 64 * scope AST node count, 8 * module AST node count))

Overrides must be positive integers. Zero, negative, and malformed values fail analysis explicitly; there is no value that disables accounting. A function's allowance does not reset for each call, fixed-point pass, or newly discovered callback context. The default also accounts for module size because a small wrapper can project large summaries from a long downstream call chain. Increasing UBS_MODULE_TIMEOUT only changes the separate wall-clock timeout and cannot increase these work limits.

Captured defaults and deferred async calls can share nested value graphs. Their immutable hashes are cached, and semantic/evidence comparisons reuse results for already compared object pairs within that comparison. A separate 64-level captured-value shape limit protects recursive value operations; exceeding it reports incomplete analysis and retains established findings. This shape limit counts both captured functions and deferred calls. Ordinary function-call chains continue to use summary convergence without a call-depth cutoff.

For example, a gate can pin both limits explicitly:

UBS_PYTHON_FLOW_MAX_STEPS=1000000 \
UBS_PYTHON_FLOW_SCOPE_STEPS=100000 \
  ubs large.py --only=python --format=json --ci

If a function exhausts its allowance, independent functions can still finish and contribute their findings. An exhausted total-file budget stops remaining work for that source. Both outcomes report incomplete coverage: the normal result is status: partial and exit 2, even when security findings are present. Diagnostics identify the exhausted function and its local source-line range when the failure occurs within a function. Established findings remain in the report after a function cutoff, including observed writes, return sources, and exception payloads projected through callers, aliases, awaited calls, and generator completions. Previously completed callback contexts contribute their observations before their contracts are revoked. Recovery consumes the remaining file allowance; it cannot certify sanitizer or return guarantees from unfinished code. A total-file cutoff retains already resolved concrete findings without completing unexplored code or new symbolic projections. Provisional summary writes are rolled back, and the partial scan is not cached as complete. A report with no findings and incomplete coverage is not a clean result.

Public regressions for issue #198 exercise large generated modules, finite loops, interprocedural flows, planted security sinks, and deliberately exhausted limits. Generated 6,568- and 9,696-line modules each check the exact locations of 13 planted semantic findings (11 taint findings and two response-header findings), alongside seven safe controls and clean variants. A separate header regression keeps all 13 planted header findings in a 6,551-line synthetic module, alongside an encoded-safe control. These fixtures reproduce the budget failure mechanism; the reporter's original private modules and planted payload are unavailable, so they do not establish a rerun of the reporter's original 13 findings.

One paired benchmark on November 2, 2026 compared the same generated sources against main commit 3d95f029 and this bounded implementation using Python 3.14.8. Both implementations ran sequentially in the same benchmark invocation with default analysis budgets. Times include parsing and direct helper analysis, not whole-CLI startup or other detectors. The fixtures come from large_budget_source in test-suite/quality/test_taint_python_worklist.py.

Source Analyzer 3d95f029 result / seconds Bounded result / seconds
6,568 lines, unsafe Taint Complete, 11/11 sites / 13.958 Complete, 11/11 sites / 1.764
6,568 lines, unsafe Header Partial, 0/2 sites / 6.445 Complete, 2/2 sites / 2.021
9,696 lines, unsafe Taint External benchmark deadline / 30.370 Complete, 11/11 sites / 4.274
9,696 lines, unsafe Header Partial, 0/2 sites / 8.330 Complete, 2/2 sites / 5.040
9,696 lines, clean Taint External benchmark deadline / 32.056 Complete, no findings / 4.014
9,696 lines, clean Header Partial / 8.121 Complete, no findings / 3.260

The baseline header failures reached its actual 250,000-expression limit. The baseline taint deadlines were imposed by the benchmark's 30-second timer, not by the scanner; the clean baseline also experienced scheduling contention and does not establish a speed ratio. On the 6,568-line taint case, module passes fell from 545 to 2 and charged work from 1,225,234 to 31,177 units. All 12 bounded runs completed with exact expected sites, including the 80-, 417-, and 1,185-line cases. Those ordinary cases took about 0.006, 0.022–0.025, and 0.077–0.085 seconds per analyzer. Peak observed process RSS among bounded runs was 60,936 KiB (59.5 MiB); this is a measurement, not a memory guarantee. The large-file regressions retain their 45-second per-case deadlines.

A further paired audit against 1d03868a found a separate setup cost: each function retained all closure names in the module. Setup now retains lexical dependencies per function and supplies escaped-callable dependencies when they are read. Incomplete recovery keeps one shared closure-name set. With Python 3.14.8 and the same 256 MiB address-space cap for both implementations:

Synthetic source 1d03868a Scoped dependency setup
1,200 independent closure pairs, 4,801 lines 2,881,200 retained names; 193,644 KiB RSS 3,600 names; 48,660 KiB RSS
1,800 pairs, 7,201 lines MemoryError during setup, before charged work Complete; exact tail sink; 57,108 KiB RSS; 0.395 s
8,000 independent functions, 16,001 lines Complete; 8.475 s Complete; 3.136 s

All nine candidate runs completed with exact unsafe/clean expectations. Completed pairs charged identical work. The memory failure was imposed by the audit cap, not a native UBS diagnostic. Whole-module state snapshots still have superlinear cost; this change removes quadratic retained closure metadata and repeated namespace scans during function initialization.

Parallel detector workers also send each completed finding before requesting the next result. A crash, timeout, missing final record, or malformed response retains the valid prefix and reports incomplete coverage. Such runs cannot populate a successful scan cache. The private worker protocol uses UTF-8 independently of the caller's terminal encoding.

The regressions for issue #192 cover 50/100/200-function subprocess chains, shuffled definitions, recursion, import cycles, standard-library files, and an actual staged scan whose worktree differs from the index. They check bounded function-solve counts as well as all expected sink locations. A separate native CLI test exhausts the default budget with recursive callback permutations and checks partial-result reporting.

Installed scans also batch the checksums of cached helpers in one hash process. Every invocation still verifies the pinned bytes; failed or malformed batches and missing or mismatched assets use the per-file verification and repair path.

When categories are skipped, parallel Python detector scheduling checks rule metadata before starting workers. A detector whose rules are all disabled needs no child interpreter. Enabled detectors still receive the full selected file set, and missing or malformed detector metadata reports incomplete analysis while independent detectors retain their findings.


🏆 Comparison with Other Tools

Feature Ultimate Bug Scanner ESLint TypeScript SonarQube DeepCode
Setup Time 30 seconds 30 minutes 1-2 hours 2-4 hours Account required
Speed (50K lines) 3 seconds 15 seconds 8 seconds 2 minutes Cloud upload
Zero Config ✅ Yes ❌ No ❌ No ❌ No ❌ No
Works Without Types ✅ Yes ✅ Yes ❌ No ✅ Yes ✅ Yes
Null Safety ✅ Yes ⚠️ Limited ✅ Yes ⚠️ Limited ⚠️ Limited
Security Scanning ✅ Yes ⚠️ Plugin ❌ No ✅ Yes ✅ Yes
Memory Leaks ✅ Yes ❌ No ❌ No ⚠️ Limited ❌ No
Async/Await ✅ Deep ⚠️ Basic ✅ Good ⚠️ Basic ⚠️ Basic
CI/CD Ready ✅ Yes ✅ Yes ✅ Yes ✅ Yes ⚠️ Cloud
Offline ✅ Yes ✅ Yes ✅ Yes ⚠️ Limited ❌ No
AI Agent Friendly ✅ Built for it ⚠️ Config heavy ⚠️ Config heavy ❌ Complex ⚠️ Cloud
Cost Free Free Free $$$$ $$$

When to use what:

  • Ultimate Bug Scanner: Quick scans, AI workflows, no config needed
  • ESLint: Style enforcement, custom rules, team standards
  • TypeScript: Type safety (use WITH this scanner)
  • SonarQube: Enterprise compliance, detailed metrics
  • DeepCode: ML-powered analysis (if you trust cloud)

Best combo: TypeScript + ESLint + Ultimate Bug Scanner = Maximum safety


🧠 Project Justification and Rationale

Why This Exists (And Why It's Not "Just Another Linter")

You might be thinking: "We already have ESLint, Pylint, Clippy, RuboCop... why build another tool?"

Fair question. And honestly, your first reaction is probably right to be skeptical.

The Initial Skepticism is Valid (But Misses the Point)

When you first look at UBS, it's natural to think:

"This is just worse ESLint. It has fewer rules, uses regex (false positives!), and doesn't auto-fix anything. Why would I use this instead of mature, comprehensive linters?"

That's analyzing through the wrong lens.

You're comparing it to tools designed for a fundamentally different workflow (human developers writing code manually) when it's solving a fundamentally different problem (LLM agents generating code at 100x speed).

It's like comparing a smoke detector to a building inspector:

  • Building inspector (ESLint): Thorough, comprehensive, finds every issue, takes hours
  • Smoke detector (UBS): Fast, catches critical dangers, instant alert, always running

You need both. But when your house might be on fire (AI just generated 500 lines in 30 seconds), you want the smoke detector first.

The Paradigm Shift: AI-Native Development

Software development is undergoing a fundamental transformation:

2020 (Pre-LLM Era):

  • Developer writes 50-200 lines/day manually
  • Deep thought before each line
  • Single language per project (mostly)
  • Time to review: abundant
  • Quality gate: comprehensive linting + code review (hours)

2025 (LLM Era):

  • AI generates 500-5000 lines/day across projects
  • Code appears in seconds
  • Polyglot projects standard (microservices in Go, UI in TypeScript, ML in Python, workers in Rust)
  • Time to review: scarce
  • Quality gate needed: instant feedback (<5s) or the loop breaks

Traditional tools weren't designed for this. They were built when "code generation" meant 200 lines/day, not 2000.

Here's the Fundamental Difference:

1. This Tool is Built FOR AI Agents, Not Just Humans

Traditional linters were designed for human developers in single-language codebases. UBS is designed for LLM coding agents working across polyglot projects.

The paradigm shift:

Traditional Linting (Human-First) UBS Approach (LLM-First)
Goal: Comprehensive coverage + auto-fix
Speed: 15-60 seconds acceptable
Setup: 30 min config per language
Languages: One tool per language
False positives: Must be <1% (frustrates humans)
Output: Human-readable prose
Goal: Critical bug detection + fast feedback
Speed: <5 seconds required
Setup: Zero config (instant start)
Languages: One scan for all 14 languages
False positives: 10-20% OK (LLMs filter instantly)
Output: Structured file:line for LLM parsing

2. LLMs Don't Need Auto-Fix—They ARE the Auto-Fix Engine

Why traditional linters have auto-fix:

// ESLint flags: "Use === instead of =="
if (value == null)  // ❌

// ESLint auto-fix (rigid, no context):
if (value === null)  // ✅ Technically correct, but...

Why UBS doesn't (and shouldn't):

// UBS flags: "Type coercion bug: == should be ==="
if (value == null)  // ❌

// Claude reads the error and understands context:
if (value !== null && value !== undefined)  // ✅ Better - handles both
// OR
if (value != null)  // ✅ Or keeps == for null/undefined (intentional)

The hard part is DETECTION, not fixing. Once flagged, LLMs can:

  • Understand semantic context
  • Consider surrounding code
  • Apply the right fix (not just the mechanical one)
  • Refactor holistically

Auto-fix would be worse because it's context-free. LLMs need to know WHAT'S wrong and WHERE, then they fix it properly.

3. The Multi-Language Zero-Config Design is the Moat

Imagine asking Claude to set up quality gates for a polyglot project:

Traditional approach (15-30 min per project):

# JavaScript/TypeScript
npm install --save-dev eslint @eslint/js @typescript-eslint/parser
# Create .eslintrc.js (200 lines of config)

# Python
pip install pylint black mypy
# Create .pylintrc, pyproject.toml sections

# Rust
# Add to Cargo.toml: [lints]
# Configure clippy rules

# Go
# Install golangci-lint, create .golangci.yml

# Java
# Setup Checkstyle + PMD + SpotBugs + config XMLs

# C++
# Setup clang-tidy, create .clang-tidy config

# Ruby
# Create .rubocop.yml with 150+ lines

# Now run 7 different commands and parse 7 different output formats...

UBS approach (30 seconds):

curl -fsSL https://raw.githubusercontent.com/.../install.sh | bash
ubs .

# Done. All supported languages detected, unified report.

This matters because:

  • LLMs generate code across languages in one session (Python API → Go service → TypeScript UI → Rust worker)
  • Configuring 7 tools is error-prone for LLMs
  • Humans don't want to maintain 7 different config files
  • CI/CD pipelines want one command, one exit code

Type Narrowing Coverage Across Languages

  • TypeScript – UBS shells out to tsserver (via the bundled helper) whenever Node.js + the typescript package are available. The installer surfaces a "Type narrowing readiness" diagnostic so you immediately know if tsserver-powered guards are running. Category 7 also flags bearer tokens, webhook signatures, CSRF values, API keys, HMAC digests, and password-reset secrets compared with ==, ===, !=, or !== instead of timing-safe equality, catches request-derived object merges and dynamic key writes that can allow prototype pollution unless prototype keys are rejected, catches request-derived SQL string construction flowing into raw db.query/sequelize.query/Prisma $queryRawUnsafe execution instead of parameterized calls, catches server-side proxy and rewrite targets from createProxyMiddleware, http-proxy, proxy.web/proxy.ws, and NextResponse.rewrite when they are request-derived without proxy target validation, and catches JWT decode-only flows, explicit verification bypass options, none algorithms, and verifier calls that lack issuer/audience claim binding.
  • Rust – A Python helper inspects if let Some/Ok guard clauses and flags subsequent .unwrap()/.expect() calls outside of exiting blocks. The Rust security pass also catches bearer tokens, webhook signatures, HMAC/MAC digests, CSRF values, API keys, and password-reset secrets compared with == or != instead of subtle::ConstantTimeEq, ring::constant_time::verify_slices_are_equal, crypto_memcmp, or a reviewed constant-time helper; JWT decode/validation bypasses such as dangerous::insecure_decode, dangerous_unsafe_decode, insecure_disable_signature_validation(), validate_exp = false, validate_aud = false, and decode calls whose validation does not bind and require both issuer and audience; archive member paths flowing into extraction destinations without enclosed_name()/unpack_in()-style containment; predictable temp-file writes that use shared temp paths without tempfile or create_new(true); request-derived redirect targets flowing into redirects or Location headers without local-path or redirect host allow-list validation; request-derived values interpolated into SQL strings that reach raw sqlx/diesel/rusqlite/postgres execution sinks instead of parameterized placeholders; credentialed wildcard or reflected-origin CORS policies without an explicit trusted-origin allow-list; and request/header values flowing into non-Location response headers without CR/LF rejection, HeaderValue::from_str, encoding, or a header-safe helper. Fixtures and manifest cases keep these regressions tested.
  • Go – Category 9 now flags bearer tokens, webhook signatures, HMAC/MAC digests, CSRF values, API keys, and password-reset secrets compared with == or != instead of hmac.Equal, subtle.ConstantTimeCompare, or a reviewed constant-time helper. It also catches JWT verification bypasses such as ParseUnverified, SigningMethodNone, WithoutClaimsValidation, jwt.Parse / ParseWithClaims callbacks that trust claims without signing-method validation, verified JWTs that omit issuer/audience claim binding, request-derived reverse proxy targets flowing into httputil.NewSingleHostReverseProxy, ProxyRequest.SetURL, or Director URL mutation without HTTPS plus host allow-list validation, and request/form/header values interpolated into SQL execution or query-builder strings instead of passed as parameters.
  • Kotlin – The Java-family module scans .kt/.kts sources for if (value == null) guards that merely log and keep running before hitting value!!, and its security pass now catches request/Ktor params, request paths, and upload filenames flowing into file read/write/serve/delete sinks, request-derived values flowing into response headers without CR/LF safety, request-derived query/header URLs flowing into Ktor-style HTTP client calls, zip extraction that writes entry.name / entry.path into destination paths without containment checks, and security-sensitive tokens, CSRF nonces, API keys, OTPs, reset codes, and invite codes built from kotlin.random.Random, java.util.Random, ThreadLocalRandom, UUID.randomUUID, or predictable time material instead of SecureRandom.
  • Swift – The dedicated ubs-swift module now ships the guard-let helper directly, so optional chaining/Objective‑C bridging heuristics fire even when you run ubs --only=swift locally (no piggybacking on the Java module). It catches cases where code logs and keeps going before force-unwrapping value!, protecting iOS/macOS pipelines that blend Swift + ObjC. The Swift security pass also tracks request query, route, URL path, and upload filename values into file read/write/serve/delete sinks unless they are reduced to a basename or pass a standardized root-containment check, flags request-derived redirect targets that reach redirects or Location headers without local-url or host allow-list validation, flags request/header/cookie/content values reaching non-Location response headers without CR/LF stripping/rejection, encoding, or a header-safe helper, flags request-derived outbound URLs that reach URLSession, URLRequest handoffs, blocking URL reads, or HTTP clients without https scheme and host allow-list validation, and now flags security-sensitive tokens, CSRF nonces, API keys, OTPs, salts, and reset/invite codes built from non-cryptographic Swift randomness or predictable time/process material instead of SecRandomCopyBytes or CryptoKit-backed helpers.

Python constant-time comparisons

Python flags ordinary equality comparisons involving secret material. For issue #193, a function-local literal dictionary with a signal or *_signal field can establish that an ambiguous *_sig local holds a public uppercase categorical label. This requires a single binding and a mapping used only for literal-key reads; mutation, aliases, escapes, conditional bindings, and namespace or frame reflection prevent that proof. Unknown or dynamic signal data, security-qualified names, sensitive comparison operands, literal passwords and signatures, and keyed or unresolved digests retain their existing detection behavior.

Resource Lifecycle AST Coverage

  • Python – modules/helpers/resource_lifecycle_py.py now reasons over the AST, tracking with/async with, alias imports, and .open()/.connect() calls so ubs-python warns only when a handle is truly leaking. Pathlib Path.open() and similar patterns are handled without brittle regexes.
  • Java – New ast-grep rules (java.resource.executor-no-shutdown, java.resource.thread-no-join, java.resource.jdbc-no-close, java.resource.resultset-no-close, java.resource.statement-no-close) ensure ExecutorServices, raw Threads, java.sql.Connections, Statement/PreparedStatement/CallableStatement, and ResultSet handles all get proper shutdown/close semantics before the regex fallback ever runs.
  • C# – modules/helpers/resource_lifecycle_csharp.py, modules/helpers/type_narrowing_csharp.py, and modules/helpers/async_task_handles_csharp.py now catch disposable-handle leaks (CancellationTokenSource, stream-like readers/writers, HttpRequestMessage), null/TryGetValue guards that log but still fall through into dereferences, and Task.Run/Task.Factory.StartNew handles that are created but never observed. The C# security pass also tracks ASP.NET request/query/header/path values and upload filenames into file read/write/serve/delete sinks unless they go through Path.GetFileName or Path.GetFullPath containment checks, flags request/header/cookie/route values that reach Redirect, Response.Redirect, RedirectResult, or Location headers without local-url or host allow-list validation, flags request/query/header/form and annotated action values reaching response headers without CR/LF stripping, rejection, or encoding, and flags request/header-derived outbound URLs reaching HttpClient, HttpRequestMessage, WebRequest, WebClient, or REST-style clients without URI parsing plus scheme and host allow-list validation.
  • C++ / Rust / Ruby / Elixir – These modules already relied on ast-grep rule packs or language-tailored context passes; the “Universal AST Adoption” epic is now complete with every language module (JS, Python, Go, C++, Rust, Java, Ruby, Swift, C#, Elixir) running semantic detectors instead of fragile grep-only heuristics. C++ now tracks CGI/query/header URL values into redirect functions and Location headers unless they pass through same-origin local-path checks or explicit redirect host allow-lists, into non-Location response headers unless they reject/strip CR/LF, encode header fragments, or pass through a header-safe helper, into libcurl/common HTTP client URL sinks unless they pass through a safe outbound URL helper, and flags security-sensitive tokens, CSRF nonces, API keys, OTPs, salts, reset codes, and invite codes built from rand, random, implementation-defined random_device, Mersenne Twister-style engines, timestamps, hashes, or process IDs instead of OS/crypto-backed random bytes. Rust tracks query/header/env/CLI URL values into reqwest, ureq, surf, isahc, and request-builder sinks unless they pass through a safe outbound URL helper or equivalent URL parsing plus host allow-list validation, tracks query/header/host redirect targets into redirect responses or Location headers unless they pass through same-origin local-path checks or redirect host allow-lists, tracks request/header values into non-Location response headers unless they reject/strip CR/LF, use HeaderValue validation, encode header fragments, or pass through a header-safe helper, and tracks request-derived values interpolated into raw SQL strings that reach sqlx, diesel, rusqlite, postgres, or generic query execution sinks without parameter binding. Ruby's security pass now tracks Rack/Rails params and upload filenames into file read/write/serve/delete sinks unless the path is reduced to File.basename or guarded by File.expand_path containment checks, flags request-derived redirect targets reaching redirect_to, Sinatra/Rack redirect, or Location headers without local-url or host allow-list validation, flags request/header/cookie/env values reaching non-Location response headers without CR/LF stripping, rejection, encoding, or a header-safe helper, and flags request-derived outbound URLs reaching common Ruby HTTP clients without URI parsing plus scheme and host allow-list validation. Elixir's security pass tracks Plug/Phoenix params, request paths, and upload filenames into File.*, send_file, and send_download sinks unless the path is reduced to Path.basename or guarded by Path.expand containment checks, flags request-derived redirect targets reaching Phoenix/Plug redirects or Location headers without local-url or host allow-list validation, flags request/header/cookie values reaching non-Location response headers without CR/LF stripping, rejection, encoding, or a header-safe helper, flags request-derived outbound URLs reaching Req, HTTPoison, Finch, Tesla, hackney, Mint, or :httpc without URI parsing plus scheme and host allow-list validation, and flags Phoenix/Guardian/Joken hardcoded config secrets such as secret_key_base, signing salts, JWT/API secrets, and literal System.get_env/2 fallbacks.

Python – AST helper in action

import asyncio, subprocess

fh = open("/tmp/leaky.txt", "w")
proc = subprocess.Popen(["sleep", "1"])

async def leak_task():
    task = asyncio.create_task(asyncio.sleep(1))
    await asyncio.sleep(0.1)
    return task

asyncio.run(leak_task())
$ ./ubs --only=python test-suite/python/buggy/resource_lifecycle.py
  🔥 File handles opened without context manager/close [resource_lifecycle.py:4]
    File handle fh opened without context manager or close()
  ⚠ Popen handles not waited or terminated [resource_lifecycle.py:7]

The helper catches the unguarded file handle, zombie subprocess, and orphaned asyncio task because it walks the AST (tracking aliases and async contexts) instead of grepping for strings.

Go – AST helper validating cleanups

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
ticker := time.NewTicker(time.Millisecond * 500)
timer := time.NewTimer(time.Second)
f, _ := os.Open("/tmp/data.txt")

_ = ctx
_ = cancel
_ = ticker
_ = timer
_ = f
$ ./ubs --only=golang test-suite/golang/buggy/resource_lifecycle.go
  🔥 context.With* without deferred cancel [resource_lifecycle.go:10]
  ⚠ time.NewTicker not stopped [resource_lifecycle.go:13]
  ⚠ time.NewTimer not stopped [resource_lifecycle.go:15]
  ⚠ os.Open/OpenFile without defer Close() [resource_lifecycle.go:17]

Because the helper hashes AST positions, the manifest can assert on deterministic substrings (context/ticker/timer/file) and we avoid flakiness from color codes or log headings.

Use --skip-type-narrowing (or UBS_SKIP_TYPE_NARROWING=1) when you want to bypass all of these guard analyzers—for example on air-gapped CI environments or when validating legacy projects one language at a time.

4. Speed Enables Tight Iteration Loops

The generate → scan → fix cycle needs to be fast for AI workflows:

┌─────────────────────────────────────────┐
│  Traditional Linter (30-45 seconds)     │
├─────────────────────────────────────────┤
│  Claude generates code:        10s      │
│  Run ESLint + Pylint + ...     30s  ⏳  │
│  Claude reads findings:         5s      │
│  Claude fixes bugs:            15s      │
│  Re-run linters:               30s  ⏳  │
│  ──────────────────────────────────     │
│  Total iteration:              90s      │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│  UBS (3-5 seconds)                      │
├─────────────────────────────────────────┤
│  Claude generates code:        10s      │
│  Run UBS:                       3s  ⚡  │
│  Claude reads findings:         2s      │
│  Claude fixes bugs:            10s      │
│  Re-run UBS:                    3s  ⚡  │
│  ──────────────────────────────────     │
│  Total iteration:              28s      │
└─────────────────────────────────────────┘

3x faster feedback loop = 3x more iterations in the same time

When you're shipping 10+ features a day with AI assistance, this compounds.

5. Detecting LLM-Specific Bug Patterns

UBS targets the bugs AI agents actually generate, not every possible code smell.

Bugs LLMs frequently produce:

Pattern Why LLMs Generate It Traditional Linters
Missing await Forgets async keyword, syntax looks fine ❌ TypeScript only
Unguarded null access "Optimistic" coding - assumes happy path ⚠️ Requires strict config
eval() / code injection Reaches for "easy" dynamic solution ✅ Most flag this
Memory leaks (event listeners) Doesn't think about cleanup lifecycle ❌ ESLint plugin needed
innerHTML XSS Doesn't threat-model user input ⚠️ Security plugins only
Division by zero Doesn't consider edge cases ❌ Most miss this
Hardcoded secrets Uses placeholder, forgets to externalize ⚠️ Requires secrets scanner
Goroutine leaks Forgets context cancellation ❌ Go-specific tooling
.unwrap() panics Assumes success path ✅ Clippy catches
Buffer overflows Forgets bounds checking ⚠️ Sanitizers only

UBS is optimized for this specific threat model.

6. Novel Analysis: Deep Property Guard Correlation

This is genuinely not available in standard linters:

# Code LLM generates:
def get_theme(user):
    return user.profile.settings.theme  # ❌ Unguarded chain

# ESLint/Pylint: ✅ No error (syntactically correct)
# TypeScript: ✅ No error (if types claim non-null)

# UBS Deep Guard Analysis:
# 1. Scans for: user.profile.settings.theme (found at line 42)
# 2. Scans for: if user and user.profile and user.profile.settings
# 3. Correlates: NO MATCHING GUARD FOUND
# 4. Reports: ⚠️ Unguarded deep property access

This requires:

  • AST extraction of property chains across the file
  • AST extraction of conditional guards
  • Cross-reference matching with context awareness
  • Contextual suggestions

Nobody else does this by default because it's not a lint rule—it's a correlation analysis across multiple code patterns.

7. Complementary, Not Competitive

UBS is designed to work WITH existing tools, not replace them:

┌────────────────────────────────────────────────────┐
│  Your Quality Stack (Recommended)                  │
├────────────────────────────────────────────────────┤
│  TypeScript           → Type safety                │
│  ESLint/Clippy/etc    → Comprehensive linting      │
│  Jest/PyTest          → Unit tests                 │
│  ✨ UBS                → AI-generated bug oracle   │
│  GitHub Actions       → CI/CD integration          │
└────────────────────────────────────────────────────┘

Use UBS for:

  • ✅ Fast multi-language scanning in AI workflows
  • ✅ Critical bug detection before commits
  • ✅ Git hooks that block obviously broken code
  • ✅ Claude/Cursor/AI agent quality guardrails
  • ✅ Polyglot projects where configuring 7 linters is painful

Use ESLint/Pylint/Clippy/etc for:

  • ✅ Comprehensive style enforcement
  • ✅ Framework-specific rules (React hooks, etc.)
  • ✅ Custom team conventions
  • ✅ Auto-formatting
  • ✅ Deep single-language analysis

They solve different problems. UBS is the "smoke detector" (fast, catches critical issues). Traditional linters are the "building inspector" (thorough, catches everything).

8. The Technical Moat

What makes this hard to replicate:

Multi-layer analysis:

Layer 1: Ripgrep (regex)     → 70% of bugs in 0.5s
Layer 2: ast-grep (AST)      → Complex semantic patterns
Layer 3: Correlation logic   → Cross-pattern analysis (novel)
Layer 4: Metrics collection  → Time-series quality tracking

This combination of speed + semantic understanding + correlation is unique.

Unified multi-language runner:

  • Auto-detects 15 languages in one scan
  • Parallel execution (Go + Python + Rust simultaneously)
  • Unified JSON/SARIF output for tooling
  • Module system with lazy download/caching

LLM-optimized integration points:

  • Git hooks (block bad commits)
  • Claude Code file-write hooks
  • .cursorrules / .aiconfig integration
  • Clean structured output for LLM parsing

9. 30 Rules is Better Than 600 (For This Use Case)

You might notice: ESLint has 200+ core rules, Clippy has 600+ lints, but UBS has ~30 patterns per language.

That's intentional, not a limitation.

The 80/20 rule for AI-generated bugs:

  • 80% of production-breaking bugs come from ~30 common patterns
  • 20% of edge cases require the other 570 rules

For LLM workflows, you want:

✅ Fast scan (3s) that catches 80% of critical bugs
   ↓
   LLM fixes them immediately
   ↓
   ✅ Fast re-scan (3s) confirms fixes
   ↓
   Then run comprehensive linters (30s) for the remaining 20%

Not:

❌ Comprehensive scan (30s) that catches 100% of issues
   ↓
   LLM waits... workflow broken... context switch...
   ↓
   Slower iteration = fewer features shipped

The bugs UBS targets are:

  • Missing await (crashes)
  • Null pointer access (crashes)
  • Security holes: eval(), XSS, hardcoded secrets (breaches)
  • Memory leaks (performance degradation)
  • Race conditions (data corruption)

The bugs comprehensive linters add:

  • Inconsistent quote style (style)
  • Missing trailing commas (style)
  • Prefer const over let when not reassigned (style)
  • Function name should be camelCase (style)
  • Line too long (style)

Which matters more when AI just generated 500 lines that might have eval() and missing await everywhere?

Target the critical bugs that cost hours. Let comprehensive linters handle style in a separate pass.

10. Market Timing: This Wouldn't Have Made Sense 3 Years Ago

Why this tool exists NOW:

2021:

  • GitHub Copilot launches (single-line completions)
  • Still mostly human-written code
  • Traditional linting workflow works fine

2023:

  • ChatGPT/GPT-4 can generate full functions
  • Claude Code, Cursor emerge
  • Devs start AI-assisted workflows
  • Pain point appears: "AI is fast but buggy"

2025:

  • LLMs generate entire features in minutes
  • Multi-file refactors happen in seconds
  • Polyglot microservices are standard
  • Quality gates can't keep up with generation speed

The problem UBS solves didn't exist before LLM coding became mainstream.

This tool is perfectly timed for the AI coding explosion happening RIGHT NOW.

11. False Positive Philosophy

For human developers:

  • False positive = context switch + investigation + frustration
  • Acceptable rate: <1%

For LLM agents:

  • False positive = parse (0.1s) + analyze (0.5s) + determine safe (0.2s)
  • Acceptable rate: 10-20%

LLMs don't get frustrated. They evaluate every finding programmatically.

This means UBS can be more aggressive:

  • Flag suspicious patterns even if not 100% certain
  • Catch more bugs at the cost of some noise
  • LLMs filter false positives cognitively (for free)

Better to flag 100 issues where 20 are safe than miss 1 critical bug.


FAQ: Common Questions and Objections

Q: "Isn't this just reinventing the wheel? ESLint already exists."

A: It's not reinventing the wheel—it's building a different vehicle for a different road.

ESLint is a truck (heavy, comprehensive, hauls everything). UBS is a sports car (fast, targeted, gets you there quickly).

You wouldn't use a truck for a Formula 1 race. You wouldn't use a sports car to move furniture.

Different tools, different use cases. Use UBS for rapid AI iteration, ESLint for comprehensive quality enforcement.


Q: "Why not just contribute these patterns to existing linters?"

A: Three reasons:

1. Different design philosophy

  • Existing linters: comprehensive, human-first, single-language
  • UBS: fast, LLM-first, multi-language, correlation-based

These are fundamentally incompatible goals. ESLint would never accept "10-20% false positives are fine" or "skip auto-fix entirely."

2. Multi-language meta-runner

  • The unified runner that auto-detects 15 languages is the core innovation
  • This doesn't fit into any single linter's architecture
  • Each linter project has different maintainers, philosophies, release cycles

3. Correlation analysis is novel

  • Deep property guard matching isn't a "lint rule"
  • It's cross-pattern analysis that requires a different architecture
  • Existing linters don't have this capability baked into their core

Contributing patterns misses the point—the integration IS the innovation.


Q: "What about Semgrep? Doesn't it do multi-language pattern matching?"

A: Semgrep is excellent and closer to UBS than traditional linters. Key differences:

Feature Semgrep UBS
Setup Requires config file + rule selection Zero config
Speed ~10-20s on medium projects ~3s (optimized for speed)
Target user Security teams, human developers LLM agents
Rule focus Security + custom patterns AI-generated bug patterns
Multi-language ✅ Yes ✅ Yes
Correlation analysis ❌ Pattern matching only ✅ Deep guards, metrics
LLM integration Not designed for it Purpose-built

Use Semgrep if: You need custom security rules and have time to configure them. Use UBS if: You want instant AI workflow integration with zero setup.

They're complementary. Some users run both.


Q: "Won't regex-based detection have tons of false positives?"

A: Less than you'd think, and it's acceptable for LLM consumers.

Reality check:

  • Layer 1 (ripgrep/regex): ~15-20% false positive rate on some patterns
  • Layer 2 (ast-grep/AST): ~2-5% false positive rate (semantic understanding)
  • Layer 3 (correlation): ~1-3% false positive rate (contextual analysis)

Blended approach: ~8-12% overall false positive rate.

Why this is OK:

  • LLMs don't get frustrated like humans do
  • They evaluate findings in 0.8 seconds total
  • Better to flag 100 (20 safe) than miss 1 critical bug
  • Humans reviewing AI code ALREADY have to check everything anyway

For critical patterns (eval, XSS, hardcoded secrets), we use ast-grep (high precision). For style patterns, we use regex (fast, some FP acceptable).

And we're always improving. Each release reduces FP rate through better heuristics.


Q: "Why Bash? Why not Python/Rust/Go?"

A: Controversial choice, but intentional:

Advantages of Bash:

  • ✅ Zero dependencies - runs on any Unix-like system
  • ✅ Universal availability - every dev machine has Bash 4.0+
  • ✅ Shell integration - git hooks, CI/CD, file watchers are natural
  • ✅ Module system - each language scanner is standalone
  • ✅ Rapid prototyping - adding new patterns is trivial
  • ✅ LLM-readable - AI agents can understand and modify rules

Disadvantages:

  • ❌ Not as "elegant" as Python
  • ❌ String handling can be verbose
  • ❌ No static typing

Bottom line: For a tool that orchestrates existing CLI tools (ripgrep, ast-grep, jq, typos) and needs to be universally available, Bash is pragmatic.

Future: Core modules might be rewritten in Rust for speed, but the meta-runner will stay Bash for compatibility.


Q: "Can I use this if I'm NOT using AI coding tools?"

A: Absolutely! It's optimized for AI workflows, but works great for humans too.

Scenarios where humans love it:

1. Code review speed-up

# Reviewing a PR with 800+ lines
git checkout feature-branch
ubs .
# Instantly see critical issues before deep review

2. Legacy code audits

# "What's dangerous in this 10-year-old codebase?"
ubs /path/to/legacy-app
# Finds all the eval(), XSS, memory leaks

3. Learning new languages

# "I'm new to Rust, what am I doing wrong?"
ubs . --verbose
# Shows common Rust pitfalls in your code

4. Polyglot projects

# Microservices in 5 languages
ubs .
# One scan, all languages checked

Humans appreciate:

  • Zero setup (no config files to maintain)
  • Fast feedback (3s vs 30s)
  • Multi-language support (one command)
  • Finding bugs ESLint misses (deep guards)

Q: "How is this different from security scanners like Snyk or GitHub Advanced Security?"

A: Different focus and scope:

Security scanners (Snyk, Dependabot, etc.):

  • Focus: Dependency vulnerabilities
  • Checks: npm packages, CVE databases
  • Speed: Seconds to minutes
  • Output: "Update package X to fix CVE-2024-1234"

UBS:

  • Focus: Code-level bugs in YOUR code
  • Checks: Logic errors, null safety, memory leaks, security anti-patterns
  • Speed: 3-5 seconds
  • Output: "You have unguarded null access at line 42"

They're complementary:

┌─────────────────────────────────────┐
│  Complete Security Stack            │
├─────────────────────────────────────┤
│  Snyk/Dependabot  → Dependencies    │
│  ✨ UBS            → Your code bugs │
│  SAST tools       → Deep security   │
│  GitHub Advanced  → Secrets in Git  │
└─────────────────────────────────────┘

Security scanners won't catch: "You forgot await and your async function silently fails." UBS won't catch: "Your version of lodash has a known CVE."

Use both.


Q: "Will you support language X in the future?"

A: Probably! The module system makes it easy to add languages.

Current: JavaScript/TypeScript, Python, Go, Rust, Java, Kotlin, C++, Ruby, Swift, C#, Elixir, Bash, PHP, PostgreSQL SQL, Terraform HCL (15 languages)

Roadmap considerations:

  • Scala - JVM ecosystem
  • Lua - Embedded scripts and game dev

How we prioritize:

  1. Community demand (GitHub issues)
  2. AI coding tool usage (what LLMs generate most)
  3. Module maintainer availability

Want to contribute? Writing a new module is ~800-1200 lines of Bash. Check the existing modules as templates.


Q: "What's the catch? Why is this free?"

A: No catch. It's MIT licensed.

Philosophy:

  • Built by developers, for developers
  • AI coding is exploding, quality tools should be accessible
  • Open source enables community contributions (more patterns, better detection)

Business model: None currently. This is a community project.

Future possibilities:

  • Enterprise support contracts
  • Hosted version for teams
  • Premium modules for specific frameworks

But the core tool will always be free and open source.


The Bottom Line

This isn't trying to replace ESLint. It's solving a different problem:

"How do I give LLM coding agents the ability to self-audit across 15 languages with zero configuration overhead and sub-5-second feedback?"

No existing tool does this because:

  • Traditional linters are human-first (need auto-fix, low FP tolerance)
  • They're single-language focused (polyglot = 7 different tools)
  • They're comprehensive, not fast (30s scan time kills AI iteration loops)
  • They're not designed for LLM consumption

UBS is purpose-built for the AI coding era.

Use it WITH your existing tools. Let ESLint handle style. Let TypeScript handle types. Let UBS catch the critical bugs that AI agents generate but can't see.


🧪 Development & Internals

Python Tooling (uv + CPython 3.14)

All helper scripts (manifest runner, fixtures, inline analyzers inside ubs) assume a single source of truth: CPython 3.14 managed by uv living inside .venv/ at the repo root.

# 1) Install uv (one-time)
curl -LsSf https://astral.sh/uv/install.sh | sh

# 2) Create the managed environment defined by pyproject.toml / uv.lock
uv sync --python 3.14

# 3) Activate it whenever you work in this repo (puts .venv/bin first on PATH)
source .venv/bin/activate

# 4) Run any Python entrypoint through the env
uv run python test-suite/run_manifest.py --case js-core-buggy
# ...or rely on 'python'/'python3' now that they point at .venv/bin/python3.14

[!NOTE] Shell scripts that invoke python3 (language modules under modules/, test-suite/run_all.sh, etc.) automatically pick up .venv/bin/python3 as long as the environment is activated or .venv/bin is on your PATH. The pinned pyproject.toml + uv.lock are the single source of truth for this toolchain.

Common uv-powered entrypoints:

  • uv run python test-suite/run_manifest.py --case js-core-buggy – run the manifest in CI or locally without manually activating the venv.
  • source .venv/bin/activate && python -m pip list – verify that every inline python3 invocation maps to CPython 3.14.
  • uv run python - <<'PY' … – mirrors how the language modules embed Python helpers, but now guaranteed to execute inside the managed interpreter.

The Self-Scan Gate

UBS has to pass its own scan. The gate is one command:

./ubs . --ci --fail-on-warning     # must exit 0: 0 critical, 0 warnings

A warning here is either a real defect in UBS or a false positive in a rule, and both block a release — a scanner that reports noise on its own source has no standing to report it on yours. The gate runs in two places, neither of them CI:

  1. scripts/cut-release.sh runs it as a sanity check before it writes anything. A red tree aborts the release with the working tree untouched, so there is no half-bumped state to clean up. There is no opt-out.
  2. .githooks/pre-push runs the same command before a push, so a red tree is caught on the machine that made it.

Local Git Hooks (opt-in)

The hooks in .githooks/ are not installed automatically. Opt in once per clone:

git config core.hooksPath .githooks

That enables both:

Hook What it does
pre-commit Regenerates MODULE_CHECKSUMS/HELPER_CHECKSUMS and SHA256SUMS when a checksum input changed, then verifies them. Reports br doctor problems for .beads/ commits without blocking.
pre-push Runs the self-scan gate (./ubs . --ci --fail-on-warning) and refuses the push when it is red. Skipped automatically for pushes that only delete refs.

The self-scan takes roughly 20 seconds on this repo. For a push that genuinely cannot wait — a docs-only fix during an incident — override it once and fix the scan in the same session:

UBS_SKIP_SELF_SCAN=1 git push

Turn the hooks off again with git config --unset core.hooksPath.


🚫 Ignoring Paths with .ubsignore

Need repo-wide scans to ignore generated code or intentionally buggy fixtures (like this project’s test-suite/)? Drop a .ubsignore at the root.

  • Format mirrors .gitignore: one glob per line, # for comments.
  • Three directory names are decided by content instead of by name: obj/ is skipped when it holds C# build output (project.assets.json, *.AssemblyInfo.cs, Debug/Release), env/ when it is a virtualenv (pyvenv.cfg, bin/activate), bin/ when no file under it has a source extension. A Go program under bin/ or a package named env is scanned. Package-manager stores .pnpm and .bun are always skipped, as is node_modules.
  • UBS loads PROJECT/.ubsignore automatically; override with --ignore-file=/path/to/file.
  • Built-in ignores already cover node_modules, virtualenvs, dist/build/target/vendor, editor caches, and more, so you rarely need to add them yourself.
  • Use --suggest-ignore to print large top-level directories that might deserve an entry (no files are modified automatically).
  • Inline suppression works for intentional one-offs: eval("print('safe')") # ubs:ignore.
  • Every language module receives the ignore list via their --exclude flag, so skips stay consistent.
  • This repository ships with a default .ubsignore that excludes test-suite/, keeping “real” source scans noise-free.

Example:

# Ignore fixtures + build output
test-suite/
dist/
coverage/

🧭 Language Coverage Comparison

Detector Family JS / TS Python Go Rust Java Kotlin C / C++ Ruby Swift C# Elixir Bash PHP sql Terraform HCL
Archive Extraction (Zip Slip) ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ○ ○ ○ ○
Async & Concurrency Hazards ✓ ✓ ✓ ✓ ✓ ○ ✓ ✓ ○ ✓ ○ ○ ○ ○ ○
Command Injection ○ ✓ ○ ✓ ✓ ✓ ○ ○ ✓ ○ ✓ ✓ ✓ ○ ○
Constant-Time Secret Comparison ✓ ✓ ✓ ✓ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○
Credentialed Wildcard CORS ✓ ✓ ✓ ✓ ○ ○ — ○ ○ ○ ○ — ○ ○ ○
Deep Guard Correlation ✓ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ — ○ ○ ○
Disabled TLS Verification ✓ ✓ ✓ ✓ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○
HTTP Response Header Injection ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ — ○ ○ ○
Hardcoded Secrets & Default Fallbacks ✓ ✓ ✓ ✓ ○ ○ ○ ○ ○ ○ ✓ ○ ○ ○ ○
Infrastructure Security Configuration ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ✓
Insecure Cookie Flags ✓ ✓ ✓ ○ ○ ○ — ○ ○ ○ ○ — ○ ○ ○
Insecure Security Randomness ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ○ ○ ○ ○
JWT Verification Bypass ✓ ✓ ✓ ✓ ○ ○ ○ ○ ○ ○ ○ — ○ ○ ○
Open Redirect ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ — ○ ○ ○
Path Traversal ✓ ✓ ✓ ○ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ○ ✓ ○ ○
PostgreSQL Function Privileges ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ✓ ○
Resource Lifecycle & Cleanup ✓ ✓ ✓ ✓ ✓ ○ ✓ ✓ ○ ✓ ○ ○ ○ ○ ○
SQL Injection ✓ ✓ ○ ✓ ○ ○ ○ ○ ○ ✓ ✓ — ✓ ○ ○
Server-Side Request Forgery (SSRF) ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ○ ○ ○ ○
Taint & Dataflow Engine ✓ ✓ ✓ ○ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ○ ✓ ○ ○
Type Narrowing & Safe Casting ✓ ○ ○ ✓ ✓ ✓ ○ ○ ✓ ✓ ○ — ○ ○ ○
Unsafe Deserialization ○ ✓ ○ ○ ○ ○ ○ ✓ ○ ○ ○ — ✓ ○ ○

(Legend: ✓ Implemented with dual-sided regression test pairs; ○ Planned; — N/A. Full details in docs/coverage-matrix.md.)


🛡️ Safety Guards for AI Coding Agents

When AI agents modify code at speed, a single destructive command can wipe hours of work. UBS ships with a Git Safety Guard that intercepts dangerous operations before they execute, designed specifically for Claude Code but applicable to any agent workflow.

How It Works

The guard lives at .claude/hooks/git_safety_guard.py and hooks into Claude Code's command execution pipeline. Before any shell command runs, the guard parses it and blocks patterns that could cause irreversible damage:

Blocked Command Why It's Dangerous Safe Alternative
git checkout -- <file> Discards uncommitted changes permanently git stash first
git reset --hard Destroys all uncommitted work git reset --soft or git stash
git clean -f Removes untracked files forever git clean -n (dry-run) first
git push --force Rewrites shared history git push --force-with-lease
rm -rf on non-temp paths Deletes files irrecoverably Explicit temp path required
git branch -D Deletes unmerged branches git branch -d (safe delete)
git stash drop/clear Loses stashed work Manual review first

Intelligent Temp Path Detection

The guard allows rm -rf only when targeting explicit temp directories:

  • ${TMPDIR}/... (macOS/Linux temp)
  • /tmp/... and /var/tmp/...
  • System-defined temporary locations

Any rm -rf targeting project directories, home folders, or ambiguous paths is blocked with a clear explanation.

Enabling the Guard

The installer sets this up automatically when it detects Claude Code (.claude/ directory exists): it installs the guard (from the repository checkout when you run the installer locally, otherwise from the release assets, verified against the release SHA256SUMS) and registers it as a PreToolUse hook on Bash in .claude/settings.json. To set it up on demand:

# Inside the project you want protected:
bash install.sh --setup-claude-hook

# The installer creates and registers this structure:
.claude/
├── settings.json             # hooks.PreToolUse (guard) + hooks.PostToolUse (scanner)
└── hooks/
    ├── git_safety_guard.py   # Command interceptor
    └── on-file-write.sh      # Scan the file Claude just wrote

The guard produces actionable error messages explaining why a command was blocked and what to do instead, so AI agents can self-correct without human intervention.


🔧 Extended Agent Detection

Beyond the core agent integrations (Claude Code, Cursor, Codex), the installer detects and configures 12+ coding agents automatically. When run with --easy-mode, all detected agents receive UBS guardrails without prompting.

Detected Agents

Agent Detection Signal Integration Type
Claude Code .claude/ directory Hooks + rules
Cursor .cursor/ directory Rules file
Codex CLI .codex/ directory Rules file/directory
Gemini Code Assist .gemini/ directory Rules file
Windsurf .windsurf/ directory Rules file
Cline .cline/ directory Rules file
OpenCode .opencode/ directory Rules file
Aider .aider.conf.yml Lint command config
Continue .continue/ directory Rules file
GitHub Copilot VS Code extensions .github/copilot-instructions.md
TabNine ~/.tabnine/ directory Detected only — no configuration is written yet
Replit .replit file Detected only — no configuration is written yet

Aider-Specific Integration

For Aider users, the installer adds automatic linting to your configuration:

# Added to ~/.aider.conf.yml
lint-cmd: "ubs --fail-on-warning ."
auto-lint: true

This makes every Aider session run UBS automatically before completing tasks.

Session Logging

The installer logs all detected agents and configuration actions to $XDG_CONFIG_HOME/ubs/session.md. Review what was configured with:

ubs sessions          # Show last session
ubs sessions --raw    # Full log with timestamps
ubs sessions --entries=5  # Last 5 sessions

🔬 AST-Based Type Narrowing Analysis

Beyond regex pattern matching, UBS includes deep AST-based analyzers for language-specific type safety issues. These helpers provide precise line-number mapping and understand language semantics that regex cannot capture.

Supported Languages

Language Helper Location Analysis Focus
TypeScript/JavaScript modules/helpers/type_narrowing_ts.js Null guards, optional chaining, type predicates
C# modules/helpers/type_narrowing_csharp.py Null guards, TryGetValue fallthrough, dereference after failed narrowing
Rust modules/helpers/type_narrowing_rust.py Option/Result handling, unwrap usage
Kotlin modules/helpers/type_narrowing_kotlin.py Nullable types, smart casts, .kt/.kts files
Swift modules/helpers/type_narrowing_swift.py Optional binding, guard statements

How It Works

The TypeScript analyzer, for example, walks the AST to detect patterns like:

// ❌ Detected: Using property without null check
if (!user) return;
console.log(user.name);  // user might be undefined here (type not narrowed)

// ✅ Safe: Proper type guard
if (user === null || user === undefined) return;
console.log(user.name);  // user is definitely defined

The analyzer understands:

  • Type guard functions (isString(), isDefined())
  • Optional chaining (user?.name?.first)
  • Nullish coalescing (value ?? default)
  • Discriminated unions and type predicates

Disabling Type Narrowing

For faster scans when type safety isn't your focus:

ubs . --skip-type-narrowing

This falls back to basic heuristics instead of AST analysis, reducing scan time for large codebases.


⚡ ast-grep Auto-Provisioning

UBS automatically downloads and manages ast-grep for enhanced JavaScript/TypeScript analysis. This happens transparently on first use.

What Gets Downloaded

Platform Binary SHA-256 Verified
macOS ARM64 ast-grep-aarch64-apple-darwin ✓
macOS Intel ast-grep-x86_64-apple-darwin ✓
Linux ARM64 ast-grep-aarch64-unknown-linux-gnu ✓
Linux x64 ast-grep-x86_64-unknown-linux-gnu ✓
Windows x64 ast-grep-x86_64-pc-windows-msvc.exe ✓

Binaries are cached at $TOOLS_DIR/ast-grep/<version>/<platform>/ and reused across scans.

Why ast-grep Matters

Regex-only detection:

// Regex sees: "await" keyword present ✓
const result = await fetch(url);
// But misses: promise not awaited in callback
items.forEach(async (item) => {
  fetch(item.url);  // ❌ Missing await - regex can't detect this
});

ast-grep detection:

// AST analysis understands:
// - async function context
// - Promise return types
// - Missing await in nested scopes
items.forEach(async (item) => {
  fetch(item.url);  // 🔥 CRITICAL: Unhandled promise in async callback
});

Fallback Behavior

If ast-grep fails to download (network issues, unsupported platform), UBS falls back to regex-based detection gracefully. The scan continues with reduced accuracy rather than failing entirely.


🏥 Maintenance Commands

UBS includes built-in maintenance tools for environment auditing and session management.

ubs doctor

Audits your UBS installation and environment:

ubs doctor              # Run all checks
ubs doctor --fix        # Auto-repair issues where possible
ubs doctor --module-dir=/custom/path  # Check specific cache location

Checks performed:

  • curl/wget availability for downloads
  • Module cache directory is writable
  • Checksum tools available (sha256sum, shasum, or openssl)
  • Cached module integrity (SHA-256 verification)
  • ast-grep binary availability and version
  • Language module health

Example output:

🏥 UBS Environment Audit
────────────────────────
✓ curl available (curl 8.4.0)
✓ Cache directory writable (/home/user/.local/share/ubs/modules)
✓ sha256sum available
✓ 8/8 modules verified
✓ ast-grep v0.45.3 ready
✓ All checks passed

ubs sessions

View installer session history:

ubs sessions              # Last session summary
ubs sessions --entries=3  # Last 3 sessions
ubs sessions --raw        # Full unformatted log
ubs sessions --config-dir=/path  # Custom config location

Session logs capture:

  • Timestamp and duration
  • Detected coding agents
  • Configured integrations
  • Any errors or warnings
  • Environment details

ubs explain

Inspect any static analysis rule with detailed message, remediation guidance, category, calibrated confidence, and real-world buggy / clean code fixture examples from the test suite:

ubs explain py.security.open-redirect               # Human-readable ANSI text
ubs explain py.security.open-redirect --format=json # Machine-parseable JSON
ubs explain open_redirect                           # Fuzzy match / suggestions if unknown

Unknown rule IDs exit 2 with nearest fuzzy and token match suggestions so agents and developers can self-correct immediately.

An installed ubs reads rules from the helper modules in its module cache (downloaded by the first scan or by ubs doctor --fix; without them explain says so and exits 2). Fixture examples come from test-suite/, so only a source checkout shows them.


🧪 Test Suite Infrastructure

UBS includes a comprehensive manifest-driven test suite for regression testing across all supported languages.

Test Structure

test-suite/
├── manifest.json          # Test case definitions
├── run_manifest.py        # Python test runner
├── artifacts/             # Captured outputs per test
├── csharp/
│   ├── buggy/
│   └── clean/
├── python/
│   ├── buggy/            # Intentionally buggy fixtures
│   └── clean/            # Clean code fixtures
├── javascript/
│   ├── buggy/
│   └── clean/
├── golang/
├── rust/
├── java/
├── ruby/
└── cpp/

Running Tests

# Run all tests
python test-suite/run_manifest.py

# Run specific test case
python test-suite/run_manifest.py --case js-core-buggy

# List available test cases
python test-suite/run_manifest.py --list

# Stop on first failure
python test-suite/run_manifest.py --fail-fast

Manifest Format

Each test case in manifest.json specifies:

{
  "id": "python-resource-lifecycle",
  "path": "test-suite/python/buggy",
  "description": "Detect resource lifecycle issues in Python",
  "enabled": true,
  "args": ["--only=python", "--category=resource-lifecycle"],
  "expect": {
    "exit_code": 1,
    "totals": {
      "critical": {"min": 3},
      "warning": {"min": 1}
    },
    "require_substrings": ["context manager", "file handle"]
  }
}

Artifact Capture

For each test run, the runner captures:

  • stdout.log - Full scanner output
  • stderr.log - Error output
  • result.json - Parsed summary with exit code, duration, findings

This enables debugging test failures and tracking scanner behavior changes across versions.


🔄 Auto-Update System

UBS includes an opt-in update mechanism that publishes the runner and its verified dependencies together. Updates require Minisign; the published public key is embedded, so cron does not depend on an interactive shell exporting it.

How It Works

# Force immediate update check
ubs --update

# Enable automatic updates (checks on each run)
export UBS_ENABLE_AUTO_UPDATE=1
ubs .

# Disable auto-updates (default in CI mode)
export UBS_NO_AUTO_UPDATE=1
ubs .
# Or use the flag:
ubs --no-auto-update .

# Force module re-download before scanning
ubs --update-modules .

An update authenticates the release manifest, verifies the runner and optional daemon, and prepares every module, helper, shared library, and contract before switching runners. The same-version path verifies and repairs the cache too. Download or verification failure returns a nonzero status for --update and keeps the previous release usable. Concurrent scans retain their own version's dependencies, and an interrupted update can be retried safely.

Installations left with a genuine old cache by a runner-only updater migrate from explicitly recognized historical contract digests, including v5.5.3. Unknown or tampered contracts still refuse scanning; ubs doctor --fix is the explicit repair command. See runner and cache publication for the integrity and recovery guarantees.

CI Mode Behavior

When --ci is passed or CI=true is detected, auto-updates are automatically disabled to ensure reproducible builds. The scanner uses whatever version is cached, preventing mid-pipeline version changes.

Manual Updates

For controlled environments, manually trigger updates:

# Update just the modules
ubs --update-modules .

# Full self-update (fetches latest ubs binary)
FORCE_SELF_UPDATE=1 ubs .

📊 Beads/Strung JSONL Integration

For integration with Beads or similar issue-tracking systems, UBS can emit findings as newline-delimited JSON (JSONL).

Usage

# Export all findings to JSONL
ubs . --beads-jsonl=findings.jsonl

# Export only summary counts (no individual findings)
ubs . --beads-jsonl=summary.jsonl --jsonl-summary-only

Output Format

Each line is a self-contained JSON object:

{"type":"finding","severity":"critical","category":1,"file":"src/app.js","line":42,"message":"Null pointer access"}
{"type":"finding","severity":"warning","category":7,"file":"src/api.js","line":88,"message":"Potential XSS vector"}
{"type":"summary","totals":{"critical":1,"warning":1,"info":0},"timestamp":"2025-01-05T12:00:00Z"}

Integration Pattern

# Pipe directly to beads for automatic issue creation
ubs . --beads-jsonl=/dev/stdout | bd import --from-jsonl

# Or append to existing tracking file
ubs . --beads-jsonl=.beads/scan-results.jsonl

📜 License

MIT License (with OpenAI/Anthropic Rider) — see LICENSE file

TL;DR: Use it anywhere. Modify it. Share it. Commercial use OK. No restrictions.


🙏 Acknowledgments

This project wouldn't exist without:

  • ast-grep by Herrington Darkholme - Revolutionary AST tooling that makes semantic analysis accessible
  • ripgrep by Andrew Gallant - The fastest search tool ever built
  • Open Source Communities - JavaScript, Python, Rust, Go, Java, C++, and Ruby communities for documenting thousands of bug patterns and anti-patterns over decades
  • AI Coding Tools - Claude, GPT-5, Cursor, Copilot for inspiring this tool and making development faster

📞 Support

Issues & Questions


💎 The Bottom Line

Every hour spent debugging production bugs is an hour not spent building features.

The Ultimate Bug Scanner gives you:

  • ✅ Confidence that code won't fail in production
  • ✅ Speed (catch bugs in seconds, not hours)
  • ✅ Quality gates for AI-generated code
  • ✅ Peace of mind when you deploy

One Command. Three Seconds. Zero Production Bugs.

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/main/install.sh?$(date +%s)" | bash

Then never waste another evening debugging a null pointer exception.


Ready to stop debugging and start shipping?

Install Now View on GitHub Documentation

Star this repo if it saved you from a production bug ⭐

✉️ Works Great with MCP Agent Mail

MCP Agent Mail is a Git-backed mailbox system that lets your coding agents coordinate work, hand off tasks, and keep durable histories of every change discussion.

  • UBS + MCP Agent Mail together: run ubs --fail-on-warning . as a standard guardrail step in your MCP Agent Mail workflows so any agent proposing code changes must attach a clean scan (or a summary of remaining issues) to their reply.
  • Ideal pattern: one agent writes or refactors code, a “QA agent” triggered via MCP Agent Mail runs UBS against the touched paths, then posts a concise findings report back into the same thread for humans or other agents to act on.
  • Result: your multi-agent automation keeps all the communication history in MCP Agent Mail while UBS continuously enforces fast, language-agnostic quality checks on every change.

🤝 About Contributions

About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.

探索相关项目