# 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.
- `default` is never provided by `export *`.
- If two star targets provide the same name from different modules, the result is "ambiguous". An import that names an ambiguous binding throws a `SyntaxError` during linking, per `InitializeEnvironment` in 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

```python
# bad: billing/__init__.py
from .dates import *
from .money import *
from .tax import *
```

```python
# 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",
]
```

```ts
// bad: src/billing/index.ts
export * from "./dates";
export * from "./money";
export * from "./tax";
```

```ts
// 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 write `ns.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`:
```toml
[tool.ruff.lint]
extend-select = ["F401", "F403", "F822"]
```

In `eslint.config.js`:
```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
```markdown
- 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 *` and `default`:** 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 `index` imported by its own submodules creates an import cycle. Submodules should import siblings directly. Pyright's `reportImportCycles` is 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 with `reportUnsupportedDunderAll`.

## 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__.py` is 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?](https://beta.llmgraph.in/agent-readable/ar-01.md)
- [AR-03: What happens when two functions share a name?](https://beta.llmgraph.in/agent-readable/ar-03.md)
- [CH-04: Why does a React prop stop arriving after a rename?](https://beta.llmgraph.in/agent-readable/ch-04.md)

## Chimeras that use this rule

- [CH-04: Why does a React prop stop arriving after a rename?](https://beta.llmgraph.in/agent-readable/ch-04.md)

Last verified 2026-10-30
