eslint-plugin-ai-guard 1.4.6 → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -7
- package/dist/cli/hook.js +42 -41
- package/dist/cli/hook.js.map +1 -1
- package/dist/cli/index.js +51 -48
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +42 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -41
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/server.js +43 -42
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,8 +42,18 @@
|
|
|
42
42
|
- **Hooks for Claude Code, Cursor and Codex** (`ai-guard init-hooks`): findings go back to the agent as it works, and it is stopped once before finishing on weakened checks or problems it introduced
|
|
43
43
|
- **A pre-commit gate** (`ai-guard init-git-hook`) that checks every commit, whichever agent or person wrote it
|
|
44
44
|
- **Works with Oxlint** as a JS plugin, with identical results checked in CI (`ai-guard init-oxlint`)
|
|
45
|
+
- **Package checks** (`ai-guard verify-deps`, an opt-in pre-install hook, and the `ai_guard_check_package` MCP tool): names that do not exist on npm, typosquats and npm security placeholders are blocked; install scripts, deprecations and releases from the last day are flagged
|
|
45
46
|
- **MCP server** (`ai-guard-mcp`, 9 tools) for local AI-agent workflows over STDIO
|
|
46
47
|
|
|
48
|
+
### Results on real projects
|
|
49
|
+
|
|
50
|
+
Measured on 12 November 2026 with 1.4.6. Methods, raw numbers and the commands to reproduce them: [docs/evidence.md](./docs/evidence.md).
|
|
51
|
+
|
|
52
|
+
- **0 of 1,361** real dependencies blocked (763 from 15 large open-source projects, 598 from 9 apps)
|
|
53
|
+
- **34 of 36** names from the 2017 crossenv malware campaign blocked
|
|
54
|
+
- **3.0%** of the 5,000 most-downloaded packages had a latest release under a day old, the window the fresh-release check covers
|
|
55
|
+
- **27 of 27** rules give identical results in ESLint and Oxlint, checked in CI
|
|
56
|
+
|
|
47
57
|
It is engineered to detect reliability bugs, async hazards, security vulnerabilities, and code-scaffolding defects frequently introduced during AI-assisted development (GitHub Copilot, Cursor, Claude Code, Gemini Code Assist, etc.).
|
|
48
58
|
|
|
49
59
|
AI Guard analyzes Abstract Syntax Trees (AST) using ESLint's native engine. It runs locally in your editor, in your terminal via the zero-config CLI, and in your CI/CD pipelines via native SARIF 2.1.0 integration with GitHub Code Scanning.
|
|
@@ -142,7 +152,7 @@ AI Guard includes **27 deterministic rules** divided into four specialized categ
|
|
|
142
152
|
|
|
143
153
|
| Rule | Recommended | What It Catches | Fixable? |
|
|
144
154
|
| :--- | :---: | :--- | :---: |
|
|
145
|
-
| [`no-empty-catch`](./docs/rules/no-empty-catch.md) | **`error`** | Empty `catch (e) {}` blocks that silently swallow exceptions without logging or rethrowing. |
|
|
155
|
+
| [`no-empty-catch`](./docs/rules/no-empty-catch.md) | **`error`** | Empty `catch (e) {}` blocks that silently swallow exceptions without logging or rethrowing. | No (editor suggestion: rethrow) |
|
|
146
156
|
| [`no-broad-exception`](./docs/rules/no-broad-exception.md) | `warn` | Catching broad exception types like `catch (e: any)` that mask system faults and typing. | No |
|
|
147
157
|
| [`no-catch-log-rethrow`](./docs/rules/no-catch-log-rethrow.md) | `off`* | Catch blocks that only log to `console` and rethrow without adding context or diagnostic info. | No |
|
|
148
158
|
| [`no-catch-without-use`](./docs/rules/no-catch-without-use.md) | `off`* | Caught error variables that are declared in catch parameters but never referenced. | No |
|
|
@@ -152,7 +162,7 @@ AI Guard includes **27 deterministic rules** divided into four specialized categ
|
|
|
152
162
|
|
|
153
163
|
| Rule | Recommended | What It Catches | Fixable? |
|
|
154
164
|
| :--- | :---: | :--- | :---: |
|
|
155
|
-
| [`no-floating-promise`](./docs/rules/no-floating-promise.md) | **`error`** | Async function invocations without `await`, `.catch()`, or `return` — leading to silent dropped errors. |
|
|
165
|
+
| [`no-floating-promise`](./docs/rules/no-floating-promise.md) | **`error`** | Async function invocations without `await`, `.catch()`, or `return` — leading to silent dropped errors. | No (editor suggestions: `await`, or `void` if intended) |
|
|
156
166
|
| [`no-async-array-callback`](./docs/rules/no-async-array-callback.md) | `warn` | Async callbacks passed to `map()`, `filter()`, `forEach()`, or `reduce()` returning `Promise[]`. | No |
|
|
157
167
|
| [`no-await-in-loop`](./docs/rules/no-await-in-loop.md) | `warn` | Sequential `await` in loops where iterations can be safely executed concurrently with `Promise.all`. | **Yes** (rewrites to `Promise.all`) |
|
|
158
168
|
| [`no-async-without-await`](./docs/rules/no-async-without-await.md) | `warn` | Functions declared `async` that never execute an `await` expression, adding unnecessary Promise overhead. | No |
|
|
@@ -455,14 +465,25 @@ jobs:
|
|
|
455
465
|
cache: 'npm'
|
|
456
466
|
|
|
457
467
|
- name: Run AI Guard
|
|
468
|
+
id: aiguard
|
|
458
469
|
uses: ai-guard-dev/eslint-plugin-ai-guard@v1
|
|
459
470
|
with:
|
|
460
471
|
preset: 'recommended'
|
|
461
472
|
fail-on: 'high'
|
|
462
473
|
changed-only: 'true'
|
|
463
|
-
|
|
474
|
+
|
|
475
|
+
# The Action writes SARIF; this step sends it to Code Scanning.
|
|
476
|
+
# if: always() uploads it even when the scan step fails the job.
|
|
477
|
+
- name: Upload to GitHub Code Scanning
|
|
478
|
+
if: always()
|
|
479
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
480
|
+
with:
|
|
481
|
+
sarif_file: ${{ steps.aiguard.outputs.sarif-file }}
|
|
482
|
+
category: ai-guard
|
|
464
483
|
```
|
|
465
484
|
|
|
485
|
+
The Action annotates the pull request and writes the job summary by itself. Alerts in **Security → Code scanning** need the upload step.
|
|
486
|
+
|
|
466
487
|
### Action Inputs (`action.yml`)
|
|
467
488
|
|
|
468
489
|
| Input | Description | Default |
|
|
@@ -471,7 +492,7 @@ jobs:
|
|
|
471
492
|
| `fail-on` | Severity threshold to fail CI: `high` \| `medium` \| `any` \| `none` | `'high'` |
|
|
472
493
|
| `changed-only` | Scan only files changed in this PR / commit | `'true'` |
|
|
473
494
|
| `path` | Target file or directory to scan | `'.'` |
|
|
474
|
-
| `upload-sarif` |
|
|
495
|
+
| `upload-sarif` | Always write the SARIF file (`sarif-file` output) for an `upload-sarif` step, even when the scan produced none. The Action does not upload to Code Scanning by itself | `'true'` |
|
|
475
496
|
| `github-summary` | Write an execution breakdown to the GitHub Actions Job Summary | `'true'` |
|
|
476
497
|
| `working-directory`| Working directory for scanning (ideal for monorepos) | `'.'` |
|
|
477
498
|
| `package-manager` | Package manager: `auto` \| `npm` \| `pnpm` \| `yarn` | `'auto'` |
|
|
@@ -610,10 +631,10 @@ npx eslint . --fix
|
|
|
610
631
|
| Rule | Automatic Fix Behavior |
|
|
611
632
|
| :--- | :--- |
|
|
612
633
|
| `no-hardcoded-secret` | Replaces hardcoded string literal with `process.env.VARIABLE_NAME` |
|
|
613
|
-
| `no-empty-catch` | Inserts `/* TODO: handle error */` comment to prevent silent swallowing |
|
|
614
|
-
| `no-floating-promise` | Prepends `void ` expression to intentionally unawaited calls |
|
|
615
634
|
| `no-await-in-loop` | Rewrites straightforward sequential loops to `await Promise.all(...)` |
|
|
616
635
|
|
|
636
|
+
`no-empty-catch` and `no-floating-promise` have no automatic fix on purpose. A fix that inserted a comment or `void` would make the finding disappear without handling the error, and an agent running `--fix` would do exactly that. Editors offer them as suggestions instead (rethrow; `await`, or `void` for an intended fire-and-forget call), which a person applies one at a time.
|
|
637
|
+
|
|
617
638
|
---
|
|
618
639
|
|
|
619
640
|
## Benchmarks & Empirical Evaluation
|
|
@@ -712,7 +733,7 @@ npx --yes --package eslint-plugin-ai-guard ai-guard-mcp
|
|
|
712
733
|
| :--- | :--- |
|
|
713
734
|
| `ai_guard_scan_file` | Scan a single JS/TS file with structured findings |
|
|
714
735
|
| `ai_guard_scan_diff` | Scan changed files in git diff (including newly created untracked JS/TS files) |
|
|
715
|
-
| `ai_guard_scan_project` | Scan a bounded workspace directory (
|
|
736
|
+
| `ai_guard_scan_project` | Scan a bounded workspace directory (at most 500 files; fails instead of truncating) |
|
|
716
737
|
| `ai_guard_rules` | List rules and presets, or look up specific rules (`rules: ["no-floating-promise"]`) with remediation and docs link |
|
|
717
738
|
| `ai_guard_check_integrity` | Report checks weakened by current Git changes (new suppressions, skipped/removed tests, lowered configs, removed CI steps), with `tier` and `hasBlockingFindings` |
|
|
718
739
|
|
package/dist/cli/hook.js
CHANGED
|
@@ -1323,7 +1323,7 @@ var init_security = __esm({
|
|
|
1323
1323
|
});
|
|
1324
1324
|
|
|
1325
1325
|
// src/rules/reliability/no-empty-catch.ts
|
|
1326
|
-
var import_utils, createRule, noEmptyCatch;
|
|
1326
|
+
var import_utils, createRule, PLACEHOLDER_COMMENT, noEmptyCatch;
|
|
1327
1327
|
var init_no_empty_catch = __esm({
|
|
1328
1328
|
"src/rules/reliability/no-empty-catch.ts"() {
|
|
1329
1329
|
"use strict";
|
|
@@ -1332,43 +1332,47 @@ var init_no_empty_catch = __esm({
|
|
|
1332
1332
|
createRule = import_utils.ESLintUtils.RuleCreator(
|
|
1333
1333
|
(name) => `https://github.com/ai-guard-dev/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1334
1334
|
);
|
|
1335
|
+
PLACEHOLDER_COMMENT = /^\s*(?:TODO|FIXME|XXX)\b/i;
|
|
1335
1336
|
noEmptyCatch = createRule({
|
|
1336
1337
|
name: "no-empty-catch",
|
|
1337
1338
|
meta: {
|
|
1338
1339
|
type: "problem",
|
|
1339
1340
|
docs: {
|
|
1340
|
-
description: "Disallow empty catch blocks. AI tools frequently generate try/catch with empty catch bodies that silently swallow errors, hiding failures in production.
|
|
1341
|
+
description: "Disallow empty catch blocks. AI tools frequently generate try/catch with empty catch bodies that silently swallow errors, hiding failures in production. A comment explaining why the block is empty is accepted; a TODO, FIXME or XXX comment is not. There is no automatic fix, so `--fix` cannot hide the problem; an editor suggestion rethrows the error."
|
|
1341
1342
|
},
|
|
1342
|
-
fixable: "code",
|
|
1343
1343
|
hasSuggestions: true,
|
|
1344
1344
|
schema: [],
|
|
1345
1345
|
messages: {
|
|
1346
1346
|
emptyCatch: "Catch block is empty. AI tools frequently generate empty catch blocks that silently swallow errors. Handle the error (log, rethrow, or recover), or add a comment explaining why it is intentionally empty.",
|
|
1347
|
-
|
|
1347
|
+
placeholderCatch: "Catch block only has a TODO comment, so the error is still swallowed. Handle the error (log, rethrow, or recover), or replace the TODO with a comment explaining why ignoring it is safe.",
|
|
1348
|
+
rethrow: "Rethrow the error."
|
|
1348
1349
|
}
|
|
1349
1350
|
},
|
|
1350
1351
|
defaultOptions: [],
|
|
1351
1352
|
create(context) {
|
|
1353
|
+
const sourceCode = context.sourceCode;
|
|
1354
|
+
function rethrowFix(node) {
|
|
1355
|
+
if (node.param === null) {
|
|
1356
|
+
return (fixer) => fixer.replaceText(node, "catch (error) { throw error; }");
|
|
1357
|
+
}
|
|
1358
|
+
if (node.param.type === "Identifier") {
|
|
1359
|
+
const name = node.param.name;
|
|
1360
|
+
return (fixer) => fixer.replaceText(node.body, `{ throw ${name}; }`);
|
|
1361
|
+
}
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1352
1364
|
return {
|
|
1353
1365
|
CatchClause(node) {
|
|
1354
|
-
if (node.body.body.length
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
suggest: [
|
|
1365
|
-
{
|
|
1366
|
-
messageId: "addTodoHandler",
|
|
1367
|
-
fix: (fixer) => fixer.replaceText(node.body, "{ /* TODO: handle error */ }")
|
|
1368
|
-
}
|
|
1369
|
-
]
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1366
|
+
if (node.body.body.length > 0) return;
|
|
1367
|
+
const comments = sourceCode.getCommentsInside(node.body);
|
|
1368
|
+
const placeholderOnly = comments.length > 0 && comments.every((c) => PLACEHOLDER_COMMENT.test(c.value));
|
|
1369
|
+
if (comments.length > 0 && !placeholderOnly) return;
|
|
1370
|
+
const fix = rethrowFix(node);
|
|
1371
|
+
context.report({
|
|
1372
|
+
node,
|
|
1373
|
+
messageId: placeholderOnly ? "placeholderCatch" : "emptyCatch",
|
|
1374
|
+
suggest: fix ? [{ messageId: "rethrow", fix }] : []
|
|
1375
|
+
});
|
|
1372
1376
|
}
|
|
1373
1377
|
};
|
|
1374
1378
|
}
|
|
@@ -2268,28 +2272,32 @@ var init_no_floating_promise = __esm({
|
|
|
2268
2272
|
meta: {
|
|
2269
2273
|
type: "problem",
|
|
2270
2274
|
docs: {
|
|
2271
|
-
description: "Disallow calling an async function or Promise-returning function without awaiting or handling the result. AI tools frequently generate floating promises where errors disappear silently.
|
|
2275
|
+
description: "Disallow calling an async function or Promise-returning function without awaiting or handling the result. AI tools frequently generate floating promises where errors disappear silently. There is no automatic fix, because marking a call with void would hide the finding without handling the error; editor suggestions offer await (inside an async function) or void for an intentional fire-and-forget call."
|
|
2272
2276
|
},
|
|
2273
|
-
|
|
2277
|
+
hasSuggestions: true,
|
|
2274
2278
|
schema: [],
|
|
2275
2279
|
messages: {
|
|
2276
|
-
floatingPromise: "This async call is not awaited, returned, or error-handled (.catch). AI tools frequently generate floating promises, causing errors to be silently lost. Add `await`, `.catch()`, or assign the result."
|
|
2280
|
+
floatingPromise: "This async call is not awaited, returned, or error-handled (.catch). AI tools frequently generate floating promises, causing errors to be silently lost. Add `await`, `.catch()`, or assign the result.",
|
|
2281
|
+
addAwait: "Await it.",
|
|
2282
|
+
markVoid: "Mark it as intentionally not awaited (void). Errors from it will still be lost."
|
|
2277
2283
|
}
|
|
2278
2284
|
},
|
|
2279
2285
|
defaultOptions: [],
|
|
2280
2286
|
create(context) {
|
|
2287
|
+
function suggestions(statement) {
|
|
2288
|
+
const text = context.sourceCode.getText(statement.expression);
|
|
2289
|
+
const fn = context.sourceCode.getAncestors(statement).reverse().find((n) => n.type === import_utils8.AST_NODE_TYPES.FunctionDeclaration || n.type === import_utils8.AST_NODE_TYPES.FunctionExpression || n.type === import_utils8.AST_NODE_TYPES.ArrowFunctionExpression);
|
|
2290
|
+
const inAsync = !!fn && "async" in fn && fn.async;
|
|
2291
|
+
return [
|
|
2292
|
+
...inAsync ? [{ messageId: "addAwait", fix: (fixer) => fixer.replaceText(statement.expression, `await ${text}`) }] : [],
|
|
2293
|
+
{ messageId: "markVoid", fix: (fixer) => fixer.replaceText(statement.expression, `void ${text}`) }
|
|
2294
|
+
];
|
|
2295
|
+
}
|
|
2281
2296
|
return {
|
|
2282
2297
|
// Check ExpressionStatements — standalone call expressions
|
|
2283
2298
|
ExpressionStatement(node) {
|
|
2284
2299
|
if (node.expression.type === import_utils8.AST_NODE_TYPES.NewExpression && node.expression.callee.type === import_utils8.AST_NODE_TYPES.Identifier && node.expression.callee.name === "Promise") {
|
|
2285
|
-
context.report({
|
|
2286
|
-
node,
|
|
2287
|
-
messageId: "floatingPromise",
|
|
2288
|
-
fix: (fixer) => fixer.replaceText(
|
|
2289
|
-
node.expression,
|
|
2290
|
-
`void ${context.sourceCode.getText(node.expression)}`
|
|
2291
|
-
)
|
|
2292
|
-
});
|
|
2300
|
+
context.report({ node, messageId: "floatingPromise", suggest: suggestions(node) });
|
|
2293
2301
|
return;
|
|
2294
2302
|
}
|
|
2295
2303
|
if (node.expression.type !== import_utils8.AST_NODE_TYPES.CallExpression) {
|
|
@@ -2304,14 +2312,7 @@ var init_no_floating_promise = __esm({
|
|
|
2304
2312
|
return;
|
|
2305
2313
|
}
|
|
2306
2314
|
if (localAsyncInfo.isAsync || isKnownPromiseFactoryCall(callExpr) || isPromiseLikeByTypeInfo(callExpr, context)) {
|
|
2307
|
-
context.report({
|
|
2308
|
-
node,
|
|
2309
|
-
messageId: "floatingPromise",
|
|
2310
|
-
fix: (fixer) => fixer.replaceText(
|
|
2311
|
-
node.expression,
|
|
2312
|
-
`void ${context.sourceCode.getText(node.expression)}`
|
|
2313
|
-
)
|
|
2314
|
-
});
|
|
2315
|
+
context.report({ node, messageId: "floatingPromise", suggest: suggestions(node) });
|
|
2315
2316
|
}
|
|
2316
2317
|
}
|
|
2317
2318
|
};
|