faultline
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@faultlinecheck if my uncommitted changes cross any boundaries"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
faultline

Recorded live on withastro/astro while real files changed. One edit crosses a fault line (production request handling importing the dev server), one adds an allowed dependency, and both are undone at the end. The systems and rules are an example map, not the Astro team's (examples/astro). Also: what Astro's production entry loads at startup.
faultline.anzalabidi.dev · A living architecture map for any codebase. You declare the systems once. Every change after that, yours or any coding agent's, shows up as light on a map that never moves, as a sentence in the PR, and as a short answer the agent can read before it writes the wrong import.
fault init # propose systems from the repo, write faultline.yml
fault map # open the live map; it redraws as files change
fault setup # connect your agents: MCP, hooks, AGENTS.md, pre-commit
fault diff main # what this branch did to the structure, in plain English
fault footprint # what an entry point loads at startup, and where to cut it
fault check main # exit 1 if it crosses a fault line or loosens a rule (CI)
fault sync # place new folders, suggest rules learned from historynpm install -g @anzalabidi/faultline # or run any command with npx -y @anzalabidi/faultlineNode 20 or newer. The command is fault.
It answers one question at a glance: what did this change do to the shape of the system? Not which lines moved. Which boxes started talking to each other, which boundaries got crossed, and the exact imports behind each of those.
✗ Crosses a fault line: Request handling → Dev server:
packages/astro/src/core/app/origin-check.ts imports warnMissingAdapter from adapter-validation.ts.
Allowed route: app → core-shared → dev-server
(~50 tokens)Any language
JavaScript and TypeScript are parsed with oxc. Every other language goes through a small adapter: a comment- and string-aware lexer that reads the imports, plus a resolver that follows that language's own rules.
Language | Reads | Resolves through |
TypeScript, JavaScript, Astro, Vue, Svelte | imports, re-exports, dynamic imports, | relative paths, |
Python |
| package roots ( |
Go | single and grouped imports |
|
Rust |
| module tree ( |
Java, Kotlin, Scala, Groovy | imports, wildcards, Scala | declared packages; same-package types by reference |
C# (and .NET projects) |
| types visible through the file's namespaces and usings |
C, C++, Objective-C, CUDA |
| relative to the file, then the closest matching path |
Ruby |
| Ruby's lexical constant lookup ( |
PHP |
| declared namespaces and class names |
Swift |
| SPM targets from |
Dart |
|
|
Elixir |
| declared |
Lua, Haskell, Zig |
| module paths and declared modules |
Adding a language is one extractor and one resolver function; see src/lang/.
Exact and inferred edges
Some languages name files in their imports. Others (C#, Swift, same-package Java, Ruby constants) only name types. faultline tags every edge:
exact: the language's own import rules name the target file.
inferred: matched by a referenced type that the repo declares somewhere visible.
Rules only fire on exact edges. A wrong edge that blocks a commit is worse than a missing one, so an inferred reference can show on the map but never fails your build. Names that shadow platform types (String, File, View, Task, and so on) are skipped rather than guessed.
Measured accuracy
Checked against each language's own toolchain on public repos (bench/accuracy/, reproducible):
Repo | Checked against | Precision | Recall |
pallets/flask | Python | 100% | 100% |
psf/requests | Python | 100% | 100% |
encode/httpx | Python | 100% | 100% |
fastapi/fastapi | Python | 100% | 97.8% (the misses are |
BurntSushi/ripgrep |
| 100% | 100% |
tokio-rs/axum |
| 100% | 100% |
tokio-rs/tokio |
| 100% | 72.7% (the misses are bench and test crates, ignored on purpose) |
Go, the JVM languages, C# and the rest were checked by hand on hugo, okhttp, spring-petclinic, eShop, redis, jekyll, laravel, swift-composable-architecture, cats, phoenix, telescope.nvim and zls. A native-toolchain comparison for them is the next benchmark to add.
Related MCP server: coderadius
Any agent
fault setup # agents it detects in the repo
fault setup --agent all # or: claude,cursor,codex,copilot,gemini,kiro,zed,opencodeAgent | MCP server | Told mid-turn when it crosses a fault line | Instructions |
Claude Code |
| PostToolUse hook |
|
Cursor |
| stop hook sends a follow-up message |
|
OpenAI Codex |
| PostToolUse hook ( |
|
GitHub Copilot (VS Code) |
| PostToolUse hook ( |
|
Gemini CLI |
|
| |
Kiro |
|
| |
Zed |
|
| |
OpenCode |
|
| |
Windsurf, Cline | printed for their global config |
| |
Anything else |
| git pre-commit hook |
|
fault setup also installs a git pre-commit hook (fault check --staged) that refuses a commit crossing a fault line, whoever wrote it. It is idempotent: run it twice and nothing changes.
Four tools, few tokens
The MCP server has four tools, each with a description under 40 words, because every connected agent pays for the tool list in every session.
Tool | Answers |
| the architecture: systems, what each owns, dependencies, fault lines, the plan |
| which system a path belongs to, what it must not import, and an allowed route when a direct import would cross a fault line |
| what your uncommitted work did to the structure, each finding with its evidence and a fix route |
| declare a new dependency before writing it; the map and PR show planned versus actual |
The same answers are on the CLI (fault overview, fault place, fault diff --format agent, fault plan) for agents without MCP. Every answer ends with its own cost, (~N tokens).
On Astro (967 source files, 22 systems), from node bench/tokens.mjs:
What the agent needs | With faultline | Without |
Tool list, once per session | 445 | |
The whole architecture | 1,225 | 11,838 just to list the paths; ~935,000 to read the files and see the imports |
Where one new file goes and what it may import | 70 to 150 | |
What my edits did to the structure | 5 to 200 |
Does it change what agents ship?
A pilot on the Astro monorepo: 36 headless Claude Code runs on tasks built to tempt one forbidden import. Where a rule covered the tempting import, agents crossed it 3 of 6 times with no guidance, 5 of 6 times with the rule written in AGENTS.md, and 0 of 6 times with faultline, at 46 to 62% higher average cost per run (the correct fix is a refactor). One repo, one model, tasks written by the builder: read it as a pilot. Method, every diff and the caveats are in bench/agent-ab.
The map
fault map serves a local page that watches the repo and redraws as files change:
Systems view. Every system with its dependencies. New edges are green, crossed fault lines red, removed edges red and dashed, planned edges dotted blue, and changed systems get an amber outline with file counts.
Drill in. Double-click a system to see its modules, grouped by the folder they come from, with callers on the left and dependencies on the right. Edges run between folders; select a module to see its own imports.
Footprint. Pick an entry point (a package export, a bin, a
main.goormain.rs) and the map shows what it loads at startup: how much of each system, which npm packages, and the shortest import chain behind any file or package. Cut points are the files whose removal from the startup path drops the most with them. Open one directly with?entry=<export or path>.Evidence. Click an edge to see every import behind it, with the new ones highlighted.
Steer. Click an edge and choose Forbid this dependency to turn it into a rule in
faultline.yml. Click a system and Plan a new dependency. Agents read both through faultline on their next call.Timeline. Commits made during the session become steps, and so do agent turns. Scrub back through them, or compare each step to the one before.
The map is private to your machine: it binds to 127.0.0.1 and refuses writes from any other origin.
fault export and fault replay write the same UI to one HTML file you can share: a single change, or a stretch of history played back commit by commit.
faultline.yml
version: 1
systems:
- id: web
name: Web UI
description: Pages and components.
paths: [apps/web/src/**]
- id: api
name: API
paths: [services/api/**]
- id: db
name: Database
paths: [packages/db/**]
rules:
- deny: web -> db
reason: The UI talks to the API, never the database
- deny: "{web,api} -> {scripts,tools}"A file belongs to the most specific system whose glob matches it, so you can carve
src/core/**out of a broadersrc/**.Files outside every system show as Unmapped, and new ones are reported in each diff, so the map never drifts from the code without anyone noticing.
Rules take system ids or globs over ids. Type-only imports don't count unless the rule sets
types: true.fault initwrites a first draft from the directory tree.fault init --outlineprints the tree and the draft for whichever agent you use to name properly;fault init --aiasks Claude directly whenANTHROPIC_API_KEYis set.
Plan versus actual
fault plan "api -> billing: invoices need customer data"Planned dependencies live in .faultline/plan.yml, next to the code. The map draws them dotted until the imports exist. The PR comment lists each one as built or not yet, and flags any new dependency between systems that nobody planned.
Keeping the map true
A map nobody updates starts lying, and a lying map gets ignored. faultline splits the upkeep in two, on purpose.
Where code lives is a fact, so it keeps itself current. A new folder outside every system shows up on the map with a suggestion: join the system its imports go to, or become a new one. fault sync --apply (or Place on the map) writes it. Agents are told to run it; placing code never loosens a rule.
$ fault sync
1 folder outside every system
billing-lab/** (2 files) → Core utilities 2 of its 3 imports to and from other systems involve Core utilities
Suggested rules (a person decides these; add one with fault sync --rule "a -> b")
deny core-shared -> cli CLI depends on Core utilities (38 imports) and Core utilities has not imported CLI in the last 200 commits. Keeps it one-way.What may depend on what is a decision, so only people make it. Rules are suggested from history: if A imports B and B has never imported A in the last 200 commits, faultline offers deny B -> A to keep it one-way. You add it with one click or fault sync --rule. Nothing adds a rule on its own.
Loosening needs a human. An agent that hits a red line could simply delete it. So:
The Claude Code hook refuses any edit that removes or narrows a rule, moves files out of a system, ignores mapped files, or leaves
faultline.ymlinvalid. Other agents are told right after the edit.fault checkjudges a change against the stricter of the twofaultline.ymlversions, and fails when the file gets looser. Approve it with--allow-looseningorFAULTLINE_ALLOW_LOOSENING=1.The pull request comment says so first, and the GitHub Action fails unless
allow-looseningis set, for example from a label:
- uses: anzal1/faultline@v0
with:
allow-loosening: ${{ contains(github.event.pull_request.labels.*.name, 'faultline-approved') }}In CI
# .github/workflows/faultline.yml
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
faultline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: anzal1/faultline@v0The action runs from its own source, so it needs no package registry. Every pull request gets one sticky comment: the headline, each structural change as a sentence, a Mermaid map of only the part that changed (GitHub renders it inline), plan versus actual, and the imports behind each change. The check fails when the PR crosses a fault line or loosens faultline.yml without approval. On GitLab, Bitbucket or anything else, run fault diff "$BASE" "$HEAD" --format markdown and post the output.
How it works
List files. A snapshot is a git ref (read straight from the object store, no checkout), the staged index, or the working tree.
Parse. Each file goes to its language's extractor. Results are cached by git blob hash, so after the first run a snapshot of a large repo rebuilds in a fraction of a second.
Resolve. Each import is resolved by its language's rules, and every edge is tagged exact or inferred.
Assign. Each file goes to a system and a module (the first folder under the system's root, named after its folder when a system spans several).
Aggregate and diff. File edges roll up into module and system edges, and two snapshots are compared.
Every repo named above builds its full graph in under a second on a laptop, cold, with no cache. Replaying the last 260 commits of the Astro monorepo takes about 30 seconds.
Footprint
$ fault footprint astro/app/entrypoint/prod --why zod
astro/app/entrypoint/prod packages/astro/src/core/app/entrypoints/virtual/prod.ts
Loads 159 files at startup across 11 systems.
Runtime 37 of 76
Routing 35 of 41
Request handling 30 of 59
...
npm at startup: @oslojs/encoding, clsx, cookie, devalue, html-escaper, piccolore, unstorage, zod
Cut points: stop importing the file and this many files stop loading at startup
11 astro/src/core/routing/handler.ts
...
4 astro/src/core/session/provider.ts drops unstorage
Why zod loads at startup
... > core/fetch/fetch-state.ts {FetchState} > core/encryption.ts {generateCspDigest} > core/csp/config.ts {ALGORITHMS, CspHash}Startup means static, non-type imports reachable from the entry; files reached only through import() count as on demand. Cut points come from the dominator tree of that graph: every startup path to a file passes through its dominators, so no longer importing one drops its whole subtree. It counts files and packages, not bytes or milliseconds, and a bundler may still tree-shake some of what it lists.
Limits
Imports built from runtime strings (
import(\./locale/${lang}`)`) are not followed.Edges are code dependencies. Calls over HTTP, queues or a DI container are not on the map yet.
Inferred edges come from type names. They are good enough to draw and never enforced.
Development
npm install
npm run build
npm test
node dist/cli.js -C path/to/repo mapMIT licensed.
This server cannot be deployed
Maintenance
Related MCP Connectors
Give your AI agent a persistent map of your project's structure, dependencies, and bugs.
Code intelligence platform for AI agents. 20 tools for architecture, security & impact analysis.
Codebase intelligence for AI agents — dead code, blast radius, ownership.
Codebase graphs, caller impact analysis, and recorded project context for AI coding agents.
Related MCP Servers
- AlicenseAqualityAmaintenanceProvides AI coding agents with durable architecture memory for repositories by generating structured project maps of responsibilities, relationships, and risks.628 npm1MIT

coderadiusofficial
AlicenseNot gradedqualityCmaintenanceEnables AI agents to query architecture context, data contracts, and blast radius to prevent cross-repo architectural breakage before merging.24Apache 2.0- AlicenseNot gradedqualityAmaintenanceProvides a dependency graph of any local repository with tools for change impact, transitive dependents, health audits, and more, enabling AI coding agents to see structure and refactor safely.4,912 npm4MIT
- AlicenseAqualityCmaintenanceExtracts deterministic architecture maps from codebases for AI agents, enabling queries about blast radius, routes, security findings, and production readiness without sending code anywhere.645 PyPIMIT