Custom Linter Rules for AI Agents [Cheat Sheet 2026]
Bottom Line
If agent code can reach randomness, wall-clock time, network, shell, or secrets without an explicit policy, encode that as a lint failure. Use ESLint for fast AST-local guardrails and Semgrep to spread the same policy across repos and languages.
Key Takeaways
- ›Use ESLint for AST-local bans and Semgrep for reusable cross-repo security patterns.
- ›In ESLint v9, flat config lives in
eslint.config.jsand rule tests useRuleTester. - ›Semgrep custom rules require
id,message,severity,languages, and a pattern key. - ›Ban randomness, wall-clock time, unrestricted network, shell exec, and prompt-time secrets by default.
Custom linter rules are one of the simplest ways to make AI-agent code auditable. Instead of trusting prompt discipline or PR reviews, you can encode hard bans on nondeterministic APIs, uncontrolled network access, shell execution, and secret leakage directly into CI. This cheat sheet uses ESLint v9 for JavaScript rule authoring and Semgrep for cross-repo pattern enforcement, with commands, config templates, UI helpers, and testing shortcuts you can lift into your own toolchain.
- Use ESLint for AST-local bans and Semgrep for reusable cross-repo security patterns.
- In ESLint v9, flat config lives in
eslint.config.jsand rule tests use RuleTester. - Semgrep custom rules require
id,message,severity,languages, and a pattern key. - Ban randomness, wall-clock time, unrestricted network, shell exec, and prompt-time secrets by default.
What To Enforce
Bottom Line
If an agent path can touch randomness, time, network, shell, or secrets without an explicit allowlist, treat it as a build-breaking lint violation.
Security Guardrails
- Ban direct
child_processexecution from agent orchestration code unless a wrapper module is approved. - Reject raw
fetch()or SDK calls unless the hostname or client is allowlisted. - Flag prompt construction that interpolates environment secrets, tokens, or credential-like constants.
- Disallow filesystem writes outside designated scratch paths for autonomous tasks.
Determinism Guardrails
- Reject
Math.random()and similar entropy sources in planning, routing, and tool-selection paths. - Reject
Date.now()andnew Date()where replayable execution matters; require injected clocks instead. - Ban implicit process state such as unscoped
process.envaccess inside core decision logic. - Require stable wrappers for retries, backoff, and concurrency so behavior is testable under fixtures.
Commands By Purpose
Author And Run Local Rules
npx eslint .
npx eslint . --fix
npx eslint --inspect-config src/agent.js
Run A Shared Semgrep Rule Pack
semgrep scan --config semgrep/rules .
semgrep scan --config p/javascript --config semgrep/rules .
semgrep scan --config semgrep/rules --metrics=off .
Test Rules Before CI
node tests/no-nondeterminism.test.js
semgrep --test --config semgrep/rules/ semgrep/tests/
semgrep --validate --config semgrep/rules/no-secrets.yaml
Export Findings For Review Tooling
npx eslint . -f json -o reports/eslint.json
semgrep scan --config semgrep/rules --json --output reports/semgrep.json .
semgrep scan --config semgrep/rules --sarif-output reports/semgrep.sarif .
- Use --fix only for deterministic rewrites such as replacing banned helpers with approved wrappers.
- Use --inspect-config to confirm the exact rule set applied to a target file in ESLint v9.
- Use --validate and --test to keep a growing Semgrep rule library sane.
Configuration
ESLint v9 Flat Config
ESLint custom rules are documented in the official custom rules guide, and plugin wiring for flat config is covered in the plugin configuration docs.
const { defineConfig } = require("eslint/config");
const agentRules = require("./eslint-plugin-agent-guardrails");
module.exports = defineConfig([
{
files: ["src/agents/**/*.js"],
plugins: { agent: agentRules },
rules: {
"agent/no-nondeterminism": "error",
"agent/no-raw-network": ["error", { allowModules: ["./http-client"] }],
"agent/no-secrets-in-prompts": "error"
}
}
]);
Minimal ESLint Rule Skeleton
module.exports = {
meta: {
type: "problem",
docs: { description: "Disallow nondeterministic APIs in agent paths" },
schema: [],
messages: {
noRandom: "Use an injected deterministic source instead of {{name}}."
}
},
create(context) {
return {
CallExpression(node) {
const callee = context.sourceCode.getText(node.callee);
if (callee === "Math.random") {
context.report({
node,
messageId: "noRandom",
data: { name: callee }
});
}
}
};
}
};
Semgrep Rule Pack
Semgrep rule structure and operators are documented in the official rule syntax and pattern syntax references.
rules:
- id: agent-no-randomness
message: Avoid nondeterministic randomness in agent execution paths.
severity: HIGH
languages: [javascript, typescript]
pattern-either:
- pattern: Math.random(...)
- pattern: crypto.randomUUID(...)
- id: agent-no-direct-fetch
message: Route outbound calls through the approved client wrapper.
severity: HIGH
languages: [javascript, typescript]
pattern: fetch(...)
- For Semgrep, every rule needs
id,message,severity,languages, and one ofpattern,patterns,pattern-either, orpattern-regex. - For fixture files, scrub prompts and sample credentials before committing them; TechBytes' Data Masking Tool is a simple fit for that workflow.
Live Search Filter
A cheat sheet gets dramatically more useful when readers can filter commands by tool, risk domain, or task. The pattern below keeps the UI static-site friendly and works with a sticky ToC.
<input id='ruleFilter' type='search' placeholder='Filter commands, rules, or flags' />
<div id='ruleCards'>
<article data-tags='eslint deterministic random rule tester'>ESLint: ban randomness</article>
<article data-tags='semgrep secrets prompt security'>Semgrep: prompt secret rule</article>
<article data-tags='network fetch allowlist security'>Network allowlist checks</article>
</div>
<script>
const input = document.getElementById('ruleFilter');
const cards = [...document.querySelectorAll('#ruleCards [data-tags]')];
input.addEventListener('input', (event) => {
const query = event.target.value.trim().toLowerCase();
cards.forEach((card) => {
const haystack = card.dataset.tags.toLowerCase();
card.hidden = query !== '' && !haystack.includes(query);
});
});
</script>
- Keep the search index in
data-tagsso you can match commands, flags, and concepts without extra parsing. - Use native
hiddentoggling to avoid layout thrash and framework overhead. - Pre-seed tags with terms developers actually search for:
random,fetch,secrets,RuleTester,--validate.
Keyboard Shortcuts
If you publish an internal lint-rule catalog, add lightweight keyboard affordances. They matter more than animation when engineers are scanning during PR review or incident cleanup.
| Shortcut | Action | Why It Helps |
|---|---|---|
/ |
Focus live filter | Fastest path to a specific rule or flag. |
Esc |
Clear filter | Resets the full command catalog instantly. |
j |
Next result | Keeps navigation keyboard-first in long references. |
k |
Previous result | Pairs naturally with j for scanning. |
c |
Copy current block | Ideal for command snippets and rule templates. |
g t |
Jump to ToC | Useful once the page grows beyond one screen. |
Advanced Usage
Test ESLint Rules With Stable Defaults
The ESLint v9 migration notes matter here: RuleTester now uses flat-config defaults unless you override them.
const { RuleTester } = require("eslint");
const rule = require("../rules/no-nondeterminism");
const tester = new RuleTester({
languageOptions: {
ecmaVersion: "latest",
sourceType: "module"
}
});
tester.run("no-nondeterminism", rule, {
valid: [{ code: "const rng = seededRandom(seed);" }],
invalid: [
{
code: "const x = Math.random();",
errors: [{ messageId: "noRandom" }]
}
]
});
Reduce False Positives Without Weakening Policy
- Prefer explicit wrapper allowlists over broad exceptions like “ignore test files.”
- In Semgrep, use
pattern-notto carve out approved variants rather than deleting the whole rule. - In ESLint, define
meta.schemaand, when needed,defaultOptionsso teams cannot misconfigure the rule silently.
Version And Publish Rule Packs Carefully
- Package ESLint rules as a plugin once more than one repo needs them.
- Use Semgrep
min-versionandmax-versionwhen a rule depends on newer syntax support. - Export JSON or SARIF in CI so findings are diffable, reviewable, and measurable over time.
Frequently Asked Questions
How do I write a custom ESLint rule for AI-agent code? +
meta and create(context), then report banned AST patterns with context.report(). In ESLint v9, wire it into eslint.config.js as a plugin and test it with RuleTester.When should I use Semgrep instead of ESLint for agent security rules? +
What are the first APIs to ban for deterministic AI agents? +
Math.random(), Date.now(), direct fetch(), raw shell execution, and secret-bearing prompt interpolation. Those five categories cover the most common ways agent runs become non-replayable or unsafe.How do I test Semgrep custom rules before shipping them? +
semgrep --test --config RULES_DIR TARGETS_DIR with annotated fixtures such as ruleid: and ok:. Run semgrep --validate --config rule.yaml as well to catch malformed or incomplete rule definitions early.Get Engineering Deep-Dives in Your Inbox
Weekly breakdowns of architecture, security, and developer tooling — no fluff.
Related Deep-Dives
ESLint Flat Config Guide for Large JS Repos
A practical guide to migrating rule packs and plugin wiring to flat config.
Security Deep-DiveSemgrep Custom Rules in CI Pipelines
How to validate, test, and ship Semgrep rules without slowing delivery.
System ArchitectureAgent Observability and Replay for Deterministic Systems
Design patterns for making AI-agent runs reproducible under audit and incident review.