Inwards: Architecture rules for Python, fast enough to run after every edit an AI agent makes.¶
You declare the layers in pyproject.toml. When an import points the wrong way, inwards check names the line and tells the agent how to fix it.
3from __future__ import annotations45from dataclasses import dataclass6from shop.infrastructure.in_memory_orders import InMemoryOrderRepository
$ uv run inwards check shop/domain/order.py
shop/domain/order.py:6:50: INW001 Layer "domain" imports "shop.infrastructure.in_memory_orders.InMemoryOrderRepository" from outer layer "infrastructure". Allowed direction: domain <- application <- infrastructure <- presentation <- bootstrap.
fix: Depend on an abstraction owned by "domain" instead of "shop.infrastructure.in_memory_orders.InMemoryOrderRepository".
1. Delete `from shop.infrastructure.in_memory_orders import InMemoryOrderRepository`. Do not move the import into a function or behind TYPE_CHECKING; Inwards checks those too.
2. Declare a typing.Protocol in `shop.domain` (for example `shop.domain.ports`) that describes only what this module needs from `InMemoryOrderRepository`.
3. Type this module against that Protocol and receive the implementation through a constructor or function parameter.
4. Make the class in "infrastructure" satisfy the Protocol, and wire it in the outermost layer (the composition root).
docs: https://sircypkowskyy.github.io/inwards/rules/INW001/
Found 1 violation in 1 file (63.7 ms).
Your architecture, as rules¶
Layers go in pyproject.toml, innermost first. INW001 enforces the direction; every diagnostic carries fix steps.
[tool.inwards]
root = "."
layers = [
{ name = "domain", modules = ["shop.domain"] },
{ name = "application", modules = ["shop.application"] },
{ name = "infrastructure", modules = ["shop.infrastructure"] },
{ name = "presentation", modules = ["shop.presentation"] },
{ name = "bootstrap", modules = ["shop.bootstrap"] },
]
Built into the agent loop¶
A check after each edit, a Stop gate over the whole session (Bash edits too), a config guard on the rules, and escalation to you after escalate-after tries.
Catches what agents do¶
Hallucinated modules, imports behind TYPE_CHECKING, dynamic imports. Suppressions need a reason (INW009); loosening the config is refused.
$ uv run inwards check shop/domain/dodge.py --format concise
shop/domain/dodge.py:8:33: INW010 "shop.domain.pricing" is not a module of this project: "shop.domain" has no "pricing". fix: Check the name. The closest modules in "shop.domain": `shop.domain.order`.
shop/domain/dodge.py:11:54: INW001 Layer "domain" imports "shop.infrastructure.in_memory_orders.InMemoryOrderRepository" from outer layer "infrastructure". Allowed direction: domain <- application <- infrastructure <- presentation <- bootstrap. fix: Delete `from shop.infrastructure.in_memory_orders import InMemoryOrderRepository`. Do not move the import into a function or behind TYPE_CHECKING; Inwards checks those too.
shop/domain/dodge.py:15:12: INW011 Layer "domain" imports "shop.infrastructure.in_memory_orders" from outer layer "infrastructure" through a dynamic import (importlib.import_module). Allowed direction: domain <- application <- infrastructure <- presentation <- bootstrap. fix: Delete `importlib.import_module("shop.infrastructure.in_memory_orders")`. A dynamic import is still a dependency: building the module name at runtime or moving it to another loader hides it instead of removing it.
Found 3 violations in 1 file (109.4 ms).
Fast enough for every edit¶
The one-file check a hook runs stays under its 100 ms p95 budget, even on a 4,482-line file.
| File | p95, of 100 ms |
|---|---|
| saleor, one file | 30 ms |
| The scaffold's example app | 39.5 ms |
| polar, a 4,482-line file | 90 ms |
inwards check <file> with process start, one thread, 2026-09-26.Cold full check, one core: 496,000 lines in 0.4 s, saleor's 848,000 in 1.8 s. Prescan misses: 0 in 6,543 files.
Quickstart¶
-
Install it, or take a binary.
-
Add the Claude Code hooks. New project? Add
--style clean --scaffold. -
Check the project.
- Get started
- Install and connect your agent.
- Rules
- Each code and its fix.
- How it works with agents
- Hooks and the Stop gate.
- Architecture and decisions
- C4 views and decisions.
Pre-alpha. Latest release: 0.7.0 on PyPI, 2026-10-11. Rules: INW000 to INW021, FAPI001 to FAPI003 and FAPI005 to FAPI009.