eslint-plugin-agent-code-guard
v0.0.5
Published
ESLint plugin that guards AI-agent-written TypeScript against common sloppy patterns: async/Promise/.then chains, bare catches, unsafe casts, raw SQL, manual enum casts, mocks in integration tests, and hardcoded secrets.
Maintainers
Readme
agent-code-guard
ESLint plugin that catches the patterns your coding agent must not ship.
npm install --save-dev eslint-plugin-agent-code-guard
pnpm add -D eslint-plugin-agent-code-guardWhat it catches
Your coding agent is miscalibrated. It was trained on human-written TypeScript — decades of it — written under one constraint that does not apply to it: typing was expensive for humans. That is why its training corpus is saturated with throw new Error("bad"), as Record<string, unknown>, try { ... } catch {}, Promise<T> return types, process.env.FOO!, raw SQL strings, and vi.mock inside integration tests. Those were the compromises humans made when keyboard time was scarce. An agent does not pay the scarcity; it inherits the patterns anyway.
This plugin is the floor. Twenty-three rules under the recommended preset (twenty errors, three warns), plus an integrationTests preset that forbids mocks in the files that are supposed to be integration tests.
| Rule | Catches |
|---|---|
| agent-code-guard/async-keyword | async functions outside Effect/Kysely patterns |
| agent-code-guard/as-unknown-as | as unknown as cast chains that bypass type checking |
| agent-code-guard/promise-type | Promise<T> return types that erase the error channel |
| agent-code-guard/then-chain | .then(...) chains that hide error propagation |
| agent-code-guard/bare-catch | try { ... } catch {} that swallows the error silently |
| agent-code-guard/effect-promise | Effect.promise(...) calls that turn rejections into defects |
| agent-code-guard/effect-error-erasure | Effect.fail(new Error(...)) and similar generic error wrapping inside the Effect channel |
| agent-code-guard/either-discriminant | Either.isLeft(...), Either.isRight(...), and _tag === "Left" / "Right" |
| agent-code-guard/manual-result | Reusable hand-rolled Result / Either algebras instead of Either / Effect |
| agent-code-guard/manual-option | Reusable hand-rolled Option / Maybe algebras instead of Option |
| agent-code-guard/manual-brand | Hand-rolled nominal brands that should use Brand.nominal(...) or Schema.brand(...) (warn) |
| agent-code-guard/manual-tagged-error | Hand-rolled tagged error classes and error unions that should use Data.TaggedError(...) |
| agent-code-guard/no-unbounded-concurrency | Effect.*(..., { concurrency: "unbounded" }) fan-out with no visible bound |
| agent-code-guard/no-process-env-at-runtime | Runtime process.env access instead of reading config once at the boundary |
| agent-code-guard/record-cast | as Record<string, unknown> and similar unsafe casts |
| agent-code-guard/no-raw-sql | Raw SQL strings that bypass the typed query builder |
| agent-code-guard/no-manual-enum-cast | as "a" \| "b" string-union casts that should be generated unions |
| agent-code-guard/no-hardcoded-secrets | AWS/GCP/Azure keys, API tokens, passwords — see doc for patterns and entropy thresholds |
| agent-code-guard/no-raw-throw-new-error | throw new Error(...) outside tests — return a tagged error instead |
| agent-code-guard/no-test-skip-only | .skip / .only / xit / xdescribe in committed test files |
| agent-code-guard/no-coverage-threshold-gate | coverageThreshold gates in jest/vitest/vite configs (warn) |
| agent-code-guard/no-hardcoded-assertion-literals | Hardcoded string/number literals in test assertions (warn) |
| agent-code-guard/tag-discriminant | Manual _tag checks on tagged errors instead of Effect.catchTag(...) |
| agent-code-guard/no-vitest-mocks | vi.mock(...) inside files that match the integration-tests glob |
Each rule ships a Before/After doc at the GitHub link above and locally at node_modules/eslint-plugin-agent-code-guard/docs/rules/<rule-name>.md.
Configure
This plugin uses ESLint flat config (required; ESLint ≥ 9). If you have a legacy .eslintrc, migrate to flat config first; see ESLint migration guide.
Flat config:
// eslint.config.js
import guard from "eslint-plugin-agent-code-guard";
import tsParser from "@typescript-eslint/parser";
export default [
// Application source — prod rules, test files excluded
{
files: ["src/**/*.ts"],
ignores: ["**/*.test.ts", "**/*.spec.ts"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: 2022, sourceType: "module" },
},
plugins: { "agent-code-guard": guard },
rules: guard.configs.recommended.rules,
},
// Test files — only the test-hygiene rule fires here
{
files: ["**/*.test.ts", "**/*.spec.ts", "**/test/**/*.ts", "**/tests/**/*.ts"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: 2022, sourceType: "module" },
},
plugins: { "agent-code-guard": guard },
rules: {
"agent-code-guard/no-test-skip-only": "error",
"agent-code-guard/no-hardcoded-assertion-literals": "warn",
},
},
// Config files — coverage-gate lint (warn)
{
files: ["**/jest.config.*", "**/vitest.config.*", "**/vite.config.*"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: 2022, sourceType: "module" },
},
plugins: { "agent-code-guard": guard },
rules: {
"agent-code-guard/no-coverage-threshold-gate": "warn",
},
},
// Integration tests: no mocks allowed
{
files: ["**/*.integration.test.ts"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: 2022, sourceType: "module" },
},
plugins: { "agent-code-guard": guard },
rules: guard.configs.integrationTests.rules,
},
];Peer dependencies: eslint ≥ 9, typescript ≥ 5.
Presets
The import alias (e.g., guard in the example above) is your choice; adjust the <import>.configs.* path accordingly. Access presets via your import identifier:
<import>.configs.recommended.rules— application source. All rules exceptno-vitest-mocks.<import>.configs.integrationTests.rules— integration-test glob only. Enforcesno-vitest-mocksso integration tests actually hit real dependencies.
Disabling a rule
If a rule is wrong for your codebase, disable it in flat config:
rules: {
...guard.configs.recommended.rules,
"agent-code-guard/async-keyword": "off",
}Every disable in source should carry a written reason via @eslint-community/eslint-plugin-eslint-comments and the require-description rule. The companion Claude Code skill (see below) wires that pairing automatically.
Name notes
- npm package:
eslint-plugin-agent-code-guard. - Rule namespace:
agent-code-guard/<rule>. The namespace matches the companion Claude Code plugin (the ceiling), not the npm package (the floor). Users who install both see a consistent mental model:agent-code-guardis the philosophy family;agent-code-guardis the npm distribution of the lint half.
Companion
floor — this ESLint plugin (lint-time checks). Catches patterns your agent must not ship: throw new Error(...), as Record<string, unknown>, bare catch {}, etc.
ceiling — the agent-code-guard Claude Code plugin. A Claude Code plugin is a skill that instruments the Claude Code IDE and directs the coding agent at write-time, before code is committed. This plugin recalibrates the agent in-band when it writes TypeScript, using the floor rules as a teaching signal.
Install both for the full calibration loop:
# The floor (this repo) — lint checks:
pnpm add -D eslint-plugin-agent-code-guard@^0.0.5
# The ceiling (Claude Code skills + binaries):
mkdir -p ~/.claude/skills
git clone --single-branch --depth 1 --branch v0.0.5 \
https://github.com/chughtapan/agent-code-guard.git \
~/.claude/skills/agent-code-guard
cd ~/.claude/skills/agent-code-guard && pnpm installAlternatively, invoke the /safer:setup skill to automate both steps on your behalf (wires floor → eslint.config.js, installs ceiling skill).
Development
pnpm install
pnpm build
pnpm testEach rule has an independent test file under tests/. The test harness uses @typescript-eslint/rule-tester.
Mutation testing
Scope: src/**/*.ts (every rule, utility, and the plugin entry). Run:
pnpm mutationStryker (with the vitest runner and typescript checker) mutates every source file and replays the vitest suite against each mutant. The default thresholds apply: high 80, low 60, break 50. A run that drops the overall score below 50 exits non-zero.
Mutation testing is a required CI gate. Every PR runs pnpm mutation; dropping below the break threshold fails the check. If you weaken a test, Stryker catches it before the lint rule ships.
Runs are incremental on PR and a full sweep runs nightly. Stryker persists state to .stryker-tmp/incremental.json, cached in CI across runs. Expected wall-clock varies with changed files and cache warmth: a small incremental rerun is usually a few minutes, while a broad sweep is closer to 15-30 minutes on a laptop.
License
MIT.
