Package Exports
- unguard
Readme
unguard
Unguard your code. Defend against overdefensive AI-generated code.
Type-aware static analysis powered by the TypeScript compiler API.
If ?? is on a non-nullable type, you don't need it.
If ?. is on a guaranteed object, it's noise.
unguard proves it with types.
Install
npm install -g unguardor simply
npx unguardUsage
unguard src # scan files/directories
unguard src --config ./unguard.config.json # load config
unguard src --ignore '**/*.gen.ts' # add ignore globs
unguard src --filter no-any-cast # run a single rule
unguard src --rule duplicate-*=warning # override rule severity/policy
unguard src --rule category:cross-file=warning
unguard src --rule tag:safety=error
unguard src --severity=error,warning # show errors+warnings
unguard src --fail-on=error # fail only on errors
unguard src --format=flat # one-line-per-diagnostic, grepable
unguard src --format=flat | grep errorAdd unguard to your lint check, especially if code is written by AI.
Config
unguard automatically loads ./unguard.config.json (or ./.unguardrc.json). Use --config <path> to specify another file.
{
"$schema": "./node_modules/unguard/schema.json",
"paths": ["src", "apps/web/src"],
"ignore": ["**/*.gen.ts", "**/routeTree.gen.ts"],
"rules": {
"duplicate-*": "warning",
"category:cross-file": "warning",
"tag:safety": "error",
"no-ts-ignore": "error",
"prefer-*": "off"
},
"failOn": "error"
}rules values can be off, info, warning, or error.
Selectors support:
- exact rule id:
no-ts-ignore - wildcard:
duplicate-* - category:
category:cross-file - tag:
tag:safety
Ignore behavior
unguard ignores:
- built-in:
node_modules,dist,.git, declaration files (*.d.ts,*.d.cts,*.d.mts) - generated files:
*.gen.*,*.generated.* - project
.gitignore - anything passed via
--ignoreor configignore
Exit codes
| Code | Meaning |
|---|---|
| 0 | No issues, or issues below --fail-on threshold |
| 1 | Failing diagnostics without errors |
| 2 | Failing diagnostics with at least one error |
Use --fail-on=error in CI to fail only on errors while still showing all diagnostics:
unguard src --fail-on=error--severity filters display only. --fail-on evaluates all diagnostics after rule policy.
Output formats
Grouped (default) -- diagnostics grouped by file:
src/lib/probe.ts
37:4 error Empty catch blocks hide failures... no-empty-catchFlat (--format=flat) -- one line per diagnostic, grepable:
src/lib/probe.ts:37:4 error [no-empty-catch] Empty catch blocks hide failures...Current Rules
Type system evasion
| Rule | Severity | What it catches |
|---|---|---|
no-any-cast |
error | x as any |
no-explicit-any-annotation |
error | param: any, const x: any |
no-inline-type-assertion |
error | x as { ... }, <{ ... }>x |
no-type-assertion |
error | x as unknown as T |
no-ts-ignore |
error | @ts-ignore / @ts-expect-error |
Defensive code (type-aware)
These rules use the TypeScript type checker. Non-nullable types suppress the diagnostic; nullable types are flagged.
| Rule | Severity | What it catches |
|---|---|---|
no-optional-property-access |
warning | obj?.prop on a non-nullable type |
no-optional-element-access |
warning | obj?.[key] on a non-nullable type |
no-optional-call |
warning | fn?.() on a non-nullable type |
no-nullish-coalescing |
warning | x ?? fallback on a non-nullable type |
no-logical-or-fallback |
warning | map.get(k) || fallback -- data-structure lookups where ?? is correct; || on numeric types that swallow 0 |
no-null-ternary-normalization |
warning | x == null ? fallback : x |
no-non-null-assertion |
warning | x! on a nullable type without a local narrowing guard |
no-double-negation-coercion |
info | !!value |
no-redundant-existence-guard |
warning | obj && obj.prop on a non-nullable type |
Error handling
| Rule | Severity | What it catches |
|---|---|---|
no-empty-catch |
error | catch {} with no body and no comment |
no-catch-return |
warning | catch { return fallback } with no logging or rethrow |
no-error-rewrap |
error | throw new Error(e.message) without { cause: e } |
Interface design
| Rule | Severity | What it catches |
|---|---|---|
no-inline-param-type |
warning | (params: { id: string; ... }) — inline object type on parameter |
prefer-default-param-value |
info | Optional param reassigned with ?? in the body |
prefer-required-param-with-guard |
info | arg?: T followed by if (!arg) throw |
repeated-literal-property |
warning | Same literal value (as const: 3+, plain: 5+) repeated in object properties within a file |
repeated-return-shape |
warning | 3+ functions return object literals with the same property name set |
State management
| Rule | Severity | What it catches |
|---|---|---|
no-module-state-write |
warning | Function mutates a module-scope binding (count++, state.ready = ..., cache.set(...)) |
Cross-file analysis
| Rule | Severity | What it catches |
|---|---|---|
duplicate-type-declaration |
warning | Same type shape in multiple files |
duplicate-type-name |
warning | Same exported type name, different shapes |
duplicate-function-declaration |
warning | Same function body in multiple files (2+ statements) |
duplicate-function-name |
warning | Same exported function name, different bodies |
duplicate-constant-declaration |
warning | Same constant value in multiple files |
duplicate-inline-type-in-params |
warning | Same inline { ... } param type shape repeated 2+ times |
duplicate-file |
warning | File with identical content to another file |
near-duplicate-function |
warning | Function bodies identical after normalizing params, strings, numbers, this |
duplicate-statement-sequence |
info | Repeated contiguous statement blocks (3+ statements) |
trivial-wrapper |
info | Function that delegates to another without transformation |
unused-export |
info | Exported function with no usages in the project |
optional-arg-always-used |
warning | Optional param provided at every call site |
explicit-null-arg |
warning | fn(null) / fn(undefined) to project functions |
dead-overload |
warning | Overload signature with zero matching project call sites |
Imports
| Rule | Severity | What it catches |
|---|---|---|
no-dynamic-import |
error | import("./module") |
Annotations
Comments near flagged lines appear in the output as context:
// intentional escape hatch for untyped AST access
type AnyNode = Record<string, any>;file.ts:2:31 error Explicit `any` annotation ... (intentional escape hatch for untyped AST access)For warning and info diagnostics, you can explicitly mark a finding as intentional with @unguard <rule-id> on the same line or immediately above:
// @unguard no-module-state-write module cache is intentional in this adapter
cache.set(user.id, user);@unguard never suppresses error diagnostics.
API
import { executeScan, scan } from "unguard";
const result = await scan({ paths: ["src/"] }); // raw diagnostics
const execution = await executeScan({
paths: ["src"],
ignore: ["**/*.gen.ts"],
rulePolicy: {
"duplicate-*": "warning",
"category:cross-file": "warning",
"tag:safety": "error",
"prefer-*": "off",
},
showSeverities: ["error", "warning"],
failOn: "error",
});
console.log(execution.exitCode);
for (const d of execution.visibleDiagnostics) {
console.log(`${d.file}:${d.line} [${d.ruleId}] ${d.message}`);
}License
MIT