Agent Rules / Dispatch and reflection
Why can't any tool see calls made through eval?
Rule AR-10
No eval/exec/Function() to call code.
eval, exec and new Function(...) take source code as a string and run it. Whatever they call is written inside a string value, so a search finds a string, a type checker sees an argument of type str, and a rename touches neither. They are also the one place where a data value can become code, which is why the standard linters flag them. Use a dispatch table (AR-08) to call a function, and a parser to read data.
Why it happens
Python. The built-in reference documents eval(source, /, globals=None, locals=None) as parsing and evaluating source as a Python expression, where source is a string or a code object. exec runs a string or code object as Python code and returns None. Both carry the same warning in the docs: they execute arbitrary code, and calling them with untrusted input leads to security vulnerabilities. The docs add that overriding __builtins__ can restrict or change available names but is not a security mechanism. For strings that contain only literals, the docs point to the literal evaluator in Python's syntax-tree module.
TypeScript and JavaScript. eval runs a string as script. ESLint's no-eval page lists the forms it reports: direct calls, indirect forms such as (0, eval)(...) and const foo = eval; foo(...), and window.eval(...) or this.eval(...) on the global object. The ECMAScript spec defines eval through an abstract operation PerformEval and a host hook, HostEnsureCanCompileStrings, that lets a host refuse to compile strings (section 19.2.1, eval ( source ), has one step: return PerformEval(source, false, false); PerformEval is 19.2.1.1, and its step 5 performs HostEnsureCanCompileStrings: https://tc39.es/ecma262/multipage/global-object.html#sec-eval-x). The Function constructor builds a function from strings. ESLint's no-new-func reports new Function(...), Function(...) without new, and the .call, .apply and .bind forms. setTimeout("code", n) and setInterval("code", n) with a string first argument are an implied eval, and no-implied-eval reports them.
For a type checker the consequence is simple. The string's content is a value, not syntax. eval(f"{name}(order)") calls a function named by name, and no tool can tell which one without knowing every value name takes. That is the AR-07 problem with no way to narrow it.
Bad and good
A rules engine where each rule is stored as a text expression.
# bad: the callee is inside a string
def apply_rule(rule: dict, order: Order):
return eval(rule["expr"], {"order": order, "discount": discount, "tax": tax})
# rule["expr"] == "discount(order, 0.1) + tax(order)"// bad: the callee is inside a string
function applyRule(rule: { expr: string }, order: Order): number {
const fn = new Function("order", "discount", "tax", `return ${rule.expr};`);
return fn(order, discount, tax);
}# good: rules name a known operation; the code lives in source
from typing import Callable, Literal
Op = Literal["discount", "tax"]
OPS: dict[Op, Callable[[Order, float], float]] = {
"discount": lambda o, rate: discount(o, rate),
"tax": lambda o, rate: tax(o),
}
def apply_rule(rule: "Rule", order: Order) -> float:
return sum(OPS[step.op](order, step.arg) for step in rule.steps)// good: rules name a known operation; the code lives in source
type Op = "discount" | "tax";
const ops = {
discount: (o: Order, rate: number) => discount(o, rate),
tax: (o: Order, _rate: number) => tax(o),
} satisfies Record<Op, (o: Order, arg: number) => number>;
function applyRule(rule: Rule, order: Order): number {
return rule.steps.reduce((sum, s) => sum + ops[s.op](order, s.arg), 0);
}The rule data becomes {op: "discount", arg: 0.1}, a value the program validates. Every operation a rule can name appears in source, so an agent that changes discount finds the table entry and the type checker checks the call.
Trade-offs and when it is acceptable
- Eval is the shortest path to a calculator or a REPL. If the product is code execution (a notebook, a plugin sandbox, a build tool that loads user scripts), it is the right tool. Keep it in one small module with a comment, and keep agents' edits away from it.
- A table is less flexible. Users can only combine operations you listed. For most configuration-driven behaviour, that limit is the feature.
- Python's literal evaluator (in the syntax-tree module) is the safe half. It reads literals (numbers, strings, lists, dicts) and raises on anything else, per the Python docs. It does not call functions.
- For JSON-shaped data, use a JSON parser. It never runs the content.
- Sandboxes don't make eval traceable. Node's
vmpage says in its introduction that the module is not a security mechanism and should not be used to run untrusted code. A sandbox changes what the code may touch. It doesn't make its calls visible to analysis. - Code generation (templates that write source files) is a separate pattern. Commit the output, or commit the generator and the command that runs it (AR-23).
Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | S307 suspicious-eval-usage | any use of the builtin eval() (suggests the standard library's literal evaluator) | 2026-10-30 |
| ruff | S102 exec-builtin | calls to the builtin exec | 2026-10-30 |
| eslint | no-eval (option allowIndirect, default false) | direct and indirect eval, window.eval, this.eval; user-defined methods named eval are not flagged | 2026-10-30 |
| eslint | no-new-func | new Function(...), Function(...), and .call, .apply, .bind forms | 2026-10-30 |
| eslint | no-implied-eval | setTimeout, setInterval and execScript with a string as first argument | 2026-10-30 |
Both ruff rules come from the flake8-bandit set; enable them with extend-select = ["S102", "S307"] in [tool.ruff.lint]. Whether ruff's S set is on by default is not stated on those rule pages, so list the two codes.
Rule block for AGENTS.md / CLAUDE.md
- Never use eval, exec, `new Function(...)` or string-argument setTimeout/setInterval to call code.
- To call a function chosen at runtime, use a typed table keyed by a literal type (see dispatch rules).
- To read data, use json.loads / JSON.parse.
- If a task seems to need eval, stop and ask; don't add it.Edge cases
- **
evalof expressions the user types** (a formula cell, a filter box): use a parser for a small grammar. A table of operations plus a tokenizer is a small amount of code and never executes user text. - **
compile()thenexec** has the same blind spot as the string forms; the source is still a value. - **Dynamic
import()andimportlib** load modules by string. That is the module-level version of AR-07; see AR-37 for components. - **Tests that
evalexpected values** hide what the test asserts. Write the value as a literal. - The literal evaluator accepts only literals. The docs list the allowed structures (strings, bytes, numbers, tuples, lists, dicts, sets, booleans,
NoneandEllipsis) and say it "is not capable of evaluating arbitrarily complex expressions, for example involving operators or indexing", so an expression like1 + 2is rejected (Python standard library docs, syntax-tree module). The docs do not say whether a complex literal such as1 + 2jis accepted.
What LLMGRAPH shows
For callees named only inside an eval string, LLMGRAPH prints no calls to it found in this repo and plus 1 dynamic call nearby that could reach anything. With a table of lambdas that name them in source, both print certain.
References
- Python library reference, built-in functions (
eval,exec): https://docs.python.org/3/library/functions.html - Ruff S307, S102: https://docs.astral.sh/ruff/rules/suspicious-eval-usage/ · https://docs.astral.sh/ruff/rules/exec-builtin/
- ESLint: https://eslint.org/docs/latest/rules/no-eval · https://eslint.org/docs/latest/rules/no-new-func · https://eslint.org/docs/latest/rules/no-implied-eval
- ECMAScript spec, global object (
eval,PerformEval,HostEnsureCanCompileStringsin the section list): https://tc39.es/ecma262/multipage/global-object.html - Node.js
vmmodule: https://nodejs.org/api/vm.html - TypeScript utility types (
Record) andsatisfies: https://www.typescriptlang.org/docs/handbook/utility-types.html · https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html
Changelog
- 2026-10-30: draft 1.
- 2026-10-30: verify items resolved (docs check)
- 2026-10-30: copy-gate wording fixes
Related
- AR-07 Why does my coding agent miss calls made through getattr?
- AR-08 How do I write dispatch that type checkers can follow?
- AR-23 How should generated code be handled for coding agents?
Last verified