Agent Rules / Names and imports
Do barrel files hide calls from coding agents?
Rule AR-02
Re-export through an explicit list (__all__, named export { }), not export * chains.
A barrel file (index.ts, or a package __init__.py) gathers names from many modules into one import path. When it does that with a wildcard, the list of names it publishes is decided by other files, and a search for a function name finds the definition and the callers but no line that connects them. An explicit list in the barrel is one short line per name, and every tool can follow it.
Why it happens
Python. A package's __init__.py is executed when the package or any of its submodules is imported, and the names it binds become attributes of the package. The language reference defines what from .a import * binds: the names in a.__all__ if it exists, otherwise every name without a leading underscore, including names a imported itself. So a wildcard barrel republishes whatever the submodules happen to expose today. The same file with an explicit list says which names are the package API, and __all__ pins the same list for later wildcard imports.
Type checkers read the explicit forms best. Pyright's typed-library guidance treats an import in a py.typed package as internal to the package unless it uses from X import A as A, import A as A, appears in __all__, or comes from a wildcard. It lists the __all__ idioms it can evaluate statically (a literal list or tuple, +=, .extend, .append, .remove, and submodule.__all__). A computed __all__ falls outside that list.
TypeScript and JavaScript. export * from "./x" re-exports every named export of ./x. The ECMAScript spec's ResolveExport defines what happens next:
- Local and explicitly forwarded exports are checked first, so a name you export by hand wins over a star.
defaultis never provided byexport *.- If two star targets provide the same name from different modules, the result is "ambiguous". An import that names an ambiguous binding throws a
SyntaxErrorduring linking, perInitializeEnvironmentin the same spec.
Neither file shows this. The barrel says only "everything from x and everything from y", and tsc 7.0.2 reports the conflict in the barrel itself, on the second export * (index.ts(2,1): error TS2308: Module "./a" has already exported a member named 'f'. Consider explicitly re-exporting to resolve the ambiguity.). A file that imports the name gets no error of its own.
A second cost is reach. If the barrel is the only import path other code uses, then an agent that renames formatAmount in money.ts has to search every file for the name, because there is no export { formatAmount } line to find in the barrel and no import line in callers that points at money.ts.
Bad and good
# bad: billing/__init__.py
from .dates import *
from .money import *
from .tax import *# good: billing/__init__.py
from .dates import month_start, month_end
from .money import format_amount, sum_rows
from .tax import apply_vat
__all__ = [
"month_start", "month_end",
"format_amount", "sum_rows",
"apply_vat",
]// bad: src/billing/index.ts
export * from "./dates";
export * from "./money";
export * from "./tax";// good: src/billing/index.ts
export { monthStart, monthEnd } from "./dates";
export { formatAmount, sumRows } from "./money";
export { applyVat } from "./tax";In the good versions, adding or renaming a public name edits one visible line, and a text search for sumRows lands on the barrel as well as the definition and the callers.
Trade-offs and when a barrel is acceptable
- A short, explicit barrel is a good thing. One import path for a package's public API keeps callers from reaching into internal files, and the explicit list is the contract.
- The explicit list costs maintenance. Each new public name needs a line. That friction is the point, since it makes widening the API a visible edit.
- **
export * as ns from "./x"** is a namespace re-export. Callers writens.name, so each use stays traceable. It is a reasonable middle ground for large modules. - Generated code (API clients, protobuf output) with hundreds of names: star re-exports are fine if the generator owns the barrel and no one edits it by hand. Keep agents out of that directory.
- Type-only barrels:
export type { Invoice } from "./types"keeps types out of the runtime output and is equally explicit. - Deep imports: if you ban imports that skip the barrel, the barrel becomes the single place to look. Pair the two rules or neither helps.
Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | F403, F405 (see AR-01) | from .x import * in __init__.py | 2026-10-30 |
| ruff | F401 unused-import | names imported in __init__.py but neither listed in __all__ nor re-exported with a redundant alias (from .x import a as a) | 2026-10-30 |
| ruff | F822 undefined-export | a name listed in __all__ that the module doesn't define | 2026-10-30 |
| pyright | reportUnsupportedDunderAll (warning in basic and standard, error in strict) | __all__ built in ways a static checker can't evaluate | 2026-10-30 |
| pyright | reportPrivateImportUsage (error in basic, standard and strict) | use of a symbol a py.typed library doesn't export | 2026-10-30 |
| eslint | no-restricted-syntax with selector ExportAllDeclaration | every export * from (the selector is the ESTree node name; the rule page doesn't list it; ESLint 9.39.5 flagged both export * from "./x.js" and export * as ns from "./y.js" with it) | 2026-10-30 |
| eslint | no-restricted-imports with patterns | imports that bypass a barrel (deep paths), and export * from of a restricted module | 2026-10-30 |
In pyproject.toml:
[tool.ruff.lint]
extend-select = ["F401", "F403", "F822"]In eslint.config.js:
{
rules: {
"no-restricted-syntax": [
"error",
{ selector: "ExportAllDeclaration[exported=null]", message: "List the exports by name." },
],
},
}The [exported=null] filter leaves export * as ns from alone. ESTree gives ExportAllDeclaration an exported field that is null for a bare star, and the selector syntax supports [attr=value] comparisons. Run with ESLint 9.39.5, the selector ExportAllDeclaration[exported=null] flagged export * from and left export * as ns from alone.
Rule block for AGENTS.md / CLAUDE.md
- Barrel files list their exports by name: `export { a, b } from "./x"` in TypeScript,
explicit imports plus `__all__` in `__init__.py`. Never add `export * from` or `from .x import *`.
- When you add or rename a public name, edit the barrel in the same change and search for the old name.
- Callers import from the package barrel, not from its internal files.Edge cases
- Conflicting names behind two stars: the spec marks the name ambiguous, so the barrel works until someone imports that one name. See AR-03 for why two functions share a name in the first place.
- **
export *anddefault:** a default export isn't forwarded by a star, so a barrel that star-exports a component file loses its default silently. Re-export it by hand:export { default as Button } from "./Button". - Circular barrels: a package's
indeximported by its own submodules creates an import cycle. Submodules should import siblings directly. Pyright'sreportImportCyclesis off in every mode by default (basic, standard, strict), so switch it on to see these. - Higher-order wrappers in a barrel: CH-04 shows a component wrapped by a HOC and re-exported. The wrapper adds props the original file doesn't declare.
- **
__all__as a concatenation** (__all__ = a.__all__ + b.__all__): pyright's supported idioms include+= submodule.__all__, but not every concatenation form, so check withreportUnsupportedDunderAll.
What LLMGRAPH shows
For calls through the wildcard barrel, LLMGRAPH's type checker still links each call (certain); grep and readers are the ones who lose it.
References
- Python Language Reference, "The import statement" (wildcard form,
__all__, public names): https://docs.python.org/3/reference/simple_stmts.html#the-import-statement - Python Language Reference, "The import system" (a package's
__init__.pyis executed when the package is imported): https://docs.python.org/3/reference/import.html - Pyright, "Typed libraries" (library interface, re-export forms, supported
__all__operations): https://github.com/microsoft/pyright/blob/main/docs/typed-libraries.md - Pyright configuration: https://github.com/microsoft/pyright/blob/main/docs/configuration.md
- Ruff F401, F822: https://docs.astral.sh/ruff/rules/unused-import/ · https://docs.astral.sh/ruff/rules/undefined-export/
- ECMAScript spec, Source Text Module Records (
ResolveExport,InitializeEnvironment): https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html - ESLint
no-restricted-syntax,no-restricted-imports, selectors: https://eslint.org/docs/latest/rules/no-restricted-syntax · https://eslint.org/docs/latest/rules/no-restricted-imports · https://eslint.org/docs/latest/extend/selectors - ESTree
ExportAllDeclaration: https://github.com/estree/estree/blob/master/es2015.md · https://github.com/estree/estree/blob/master/es2020.md - TypeScript handbook, modules reference (
export type): https://www.typescriptlang.org/docs/handbook/modules/reference.html
Changelog
- 2026-10-30: draft 1.
- 2026-10-30: tool checks resolved (tsc 7.0.2, eslint 9.39.5, ruff 0.17.3, mypy 2.4.1, pyright 1.1.414)
- 2026-10-30: copy-gate wording fixes
Related
- AR-01 Why do star imports make my coding agent miss callers?
- AR-03 What happens when two functions share a name?
- CH-04 Why does a React prop stop arriving after a rename?
Chimeras that use this rule
- CH-04 Why does a React prop stop arriving after a rename?
Last verified