Arklint
Enforce import boundaries, dependency rules, and layer constraints across your codebase. Define your architecture in YAML - Arklint checks it automatically, before violations ever reach main.
pip install arklint
npx arklint check
dotnet tool install -g arklint
.arklint.yml$ arklint check Arklint v1.0.0 - Scanning 142 files against 5 rules... ✗ FAIL no-direct-db-in-routes routes/users.py → imports 'sqlalchemy' - blocked routes/orders.py → imports 'psycopg2' - blocked ✗ FAIL single-http-client conflicting packages - keep one of requests, httpx ⚠ WARN no-print-statements services/email.py:45 → banned pattern: 'print(' ✓ PASS models-in-models-dir ✓ PASS layered-architecture ──────────────────────────────────────────────────── Results: 2 errors, 1 warning, 2 passed
Overview #
Your codebase has architectural rules. Nobody enforces them. Arklint does.
The problem nobody talks about
You’ve seen this happen. A pull request lands where a route handler imports directly from the database. Or an AI agent generates three files - clean code, wrong layer. Or requirements.txt quietly grows to include both requests and httpx because nobody was watching.
Code review catches some of this. Linters check syntax and style. But there’s nothing - until now - that enforces structural intent: which module can import which, which packages can coexist, which patterns belong in which directories.
Without enforcement, the architecture degrades. Rules exist only as comments in a wiki nobody reads.
What Arklint does
You write your architectural rules in a plain YAML file. Arklint checks them against your entire codebase in seconds - and blocks violations in CI before they ever merge.
version: "1" rules: - id: no-direct-db-in-routes type: boundary description: "Route handlers must not import database drivers directly" source: "routes/**" blocked_imports: - "sqlalchemy" - "psycopg2" severity: error - id: single-http-client type: dependency description: "Use exactly one HTTP client library" allow_only_one_of: - "requests" - "httpx" severity: error
Run the check:
$ arklint check ✗ FAIL no-direct-db-in-routes routes/users.py → imports 'sqlalchemy' - blocked routes/orders.py → imports 'psycopg2' - blocked ✗ FAIL single-http-client conflicting packages detected - keep one of: requests, httpx ✓ PASS models-in-models-dir ✓ PASS layered-architecture Results: 2 errors, 2 passed
Violations are errors. Errors fail the pipeline. The architecture stays clean.
What it checks
Arklint ships with five rule types covering the most common architectural violations:
| Rule type | What it enforces |
|---|---|
boundary |
Which directories can import which packages |
dependency |
Which libraries can coexist in your dependency file |
file-pattern |
Where specific code patterns are allowed to live |
pattern-ban |
Regex patterns that must never appear in the codebase |
layer-boundary |
The allowed import direction between architectural layers |
Where it runs
Locally - arklint check while you build; arklint watch for live feedback on every save.
Pre-commit - block violations before they’re committed.
CI - arklint check --strict fails the pipeline. --diff origin/main makes it fast on large repos by scanning only changed files.
Inside AI coding tools - the MCP server lets Claude Code, Cursor, and Copilot check code before writing it. The export command writes your rules into .cursorrules or CLAUDE.md so AI agents understand your architecture natively.
Core concepts
| Concept | Description |
|---|---|
| Rule | A single architectural constraint - one type, one id, one severity |
.arklint.yml |
The config file where all your rules live |
| Pack | A shareable bundle of rules for a framework (e.g. arklint/fastapi) |
| Check | arklint check - scans every file against every rule |
| MCP server | Exposes your rules to AI agents via the Model Context Protocol |
Get started
Python
$ pip install arklint $ arklint init $ arklint check
Node.js
$ npm install -g arklint $ arklint init $ arklint check
.NET
$ dotnet tool install -g arklint $ arklint init $ arklint check
Installation #
Arklint is available on PyPI, npm, and NuGet.
Which method should I use?
| Feature | pip (PyPI) | npm | .NET (NuGet) |
|---|---|---|---|
arklint check |
✓ | ✓ | ✓ |
arklint watch |
✓ | ✓ | ✓ |
arklint diff |
✓ | ✓ | ✓ |
arklint visualize |
✓ | ✓ | ✓ |
arklint learn |
✓ | ✓ | ✓ |
arklint mcp |
✓ (with [mcp] extra) |
✓ | ✓ |
| Python required | Yes (3.10+) | No | No |
All three methods run the same binary. Use npm or .NET if you are not in a Python project. Use pip if you want the MCP server extra or need to contribute.
Python (PyPI)
Requires Python 3.10+.
$ pip install arklint
For MCP server support (AI agent integration):
$ pip install 'arklint[mcp]'
For AI-powered rule generation with arklint learn (already bundled in npm/.NET, only needed here):
$ pip install 'arklint[ai-anthropic]' # Claude (Anthropic) $ pip install 'arklint[ai-openai]' # GPT-4o-mini (OpenAI)
ai-anthropic and ai-openai - with the ai- prefix. pip install arklint[anthropic] will fail. The --provider flag uses just anthropic / openai (no prefix) - a different naming from the pip extras.Node.js (npm)
A thin wrapper that auto-downloads the platform-specific prebuilt binary on first run. No Python required.
# Run directly without installing $ npx arklint check # Or install globally $ npm install -g arklint $ arklint check
Supports Linux x64, macOS ARM64 (Apple Silicon), and Windows x64.
.NET (NuGet)
A .NET global tool wrapper. Auto-downloads the prebuilt binary and caches it at ~/.arklint/bin/. Requires .NET 8.0+.
$ dotnet tool install -g arklint $ arklint check
Quick Start #
Get from install to your first architecture check in four steps.
1. Generate a starter config
Run arklint init in your project root. Arklint detects your ecosystem (Python, Node.js, or .NET) and generates a .arklint.yml with sensible starter rules for that stack.
$ cd ~/my-project $ arklint init
2. Edit your rules
Open .arklint.yml and tailor the rules to your architecture - import boundaries, banned patterns, layered dependencies - all in readable YAML.
version: "1" rules: - id: no-direct-db-in-routes type: boundary description: "API routes must not import database modules" source: "routes/**" blocked_imports: - "sqlalchemy" - "psycopg2" severity: error - id: no-print-statements type: pattern-ban pattern: 'print\(' exclude: - "tests/**" severity: warning
3. Run the check
Scan your codebase. Add flags as needed:
$ arklint check $ arklint check --strict # exit 1 on warnings too $ arklint check --json # JSON output for CI $ arklint check --diff main # only scan changed files
4. Add to your workflow
- Watch mode - instant feedback as you code:
arklint watch - Pre-commit hook - block violations before they’re committed
- CI gate - fail the pipeline on violations
- GitHub Action - first-class Action with diff mode and version pinning
See CI / Pre-commit and GitHub Action for ready-to-copy configs.
Rule Types #
Arklint ships with five rule types. Each targets a specific class of architectural violation. All are language-agnostic and configured in `.arklint.yml`.
Common fields
Every rule shares these fields:
| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier for the rule (e.g. no-direct-db-in-routes) |
type |
Yes | Rule type: boundary, dependency, file-pattern, pattern-ban, or layer-boundary |
description |
Yes | Human-readable explanation shown in violation output |
severity |
Yes | error (fails the check) or warning (informational unless --strict) |
exclude |
No | List of glob patterns to skip. Supported by boundary, file-pattern, and pattern-ban |
Import Restrictions
Block files in a source directory from importing specific packages. Keep API routes away from raw database drivers.
- id: no-direct-db-in-routes type: boundary description: "Route handlers must not import database drivers directly" source: "routes/**" blocked_imports: - "sqlalchemy" - "psycopg2" - "pymongo" severity: error
Package Conflicts
Detect when multiple libraries serving the same purpose exist in your dependency file.
- id: single-http-client type: dependency description: "Use exactly one HTTP client library" allow_only_one_of: - "requests" - "httpx" - "aiohttp" severity: error
Code Placement
Ensure certain code patterns only exist in the right directories.
- id: models-in-models-dir type: file-pattern description: "Model classes must live in models/ or schemas/" pattern: 'class\s+\w*Model' allowed_in: - "models/**" - "schemas/**" severity: warning
Banned Patterns
Ban any regex pattern across the codebase with optional directory exclusions.
- id: no-print-statements type: pattern-ban description: "Use a logger instead of print statements" pattern: '(?<!\.)print\(' exclude: - "tests/**" - "scripts/**" severity: warning
Layered Architecture
Define layers and control which ones can import from which. Enforce strict dependency direction.
- id: layered-architecture type: layer-boundary description: "Enforce routes → services → repositories dependency direction" layers: - name: routes path: "routes/**" - name: services path: "services/**" - name: repositories path: "repositories/**" allowed_dependencies: routes: [services] services: [repositories] repositories: [] severity: error
Rule Packs #
Rule packs are shareable bundles of arklint rules for a specific framework or architecture pattern. Use them to get started in seconds without writing rules from scratch.
Browsing packs
$ arklint search fastapi $ arklint search django $ arklint search clean
Adding a pack
$ arklint add arklint/fastapi Fetching pack 'arklint/fastapi'… ✓ Added arklint/fastapi (4 rules) to .arklint.yml Run arklint validate to confirm.
This appends the pack to the extends list in your .arklint.yml. Your own rules always take precedence - if you define a rule with the same id as a pack rule, yours wins.
Official packs
| Pack | Rules | Description |
|---|---|---|
arklint/fastapi |
4 | FastAPI service/route/schema separation, single HTTP client |
arklint/django |
4 | Django model/view/serializer placement, no raw SQL in views |
arklint/nextjs |
3 | Next.js server/client boundary, no DB in server actions |
arklint/express |
3 | Express route/service separation, no console.log in prod |
arklint/clean-arch |
2 | Clean architecture layer ordering, no framework in entities |
Using extends manually
# .arklint.yml version: "1" extends: - arklint/fastapi - arklint/clean-arch - ./local-packs/my-company.yml # local file packs also work rules: # project-specific rules - these override pack rules with the same id - id: fastapi/no-db-in-routes type: boundary description: "Customised version of the pack rule" source: "api/**" blocked_imports: ["sqlalchemy", "psycopg2", "motor"] severity: error
Local packs
Point extends at a relative path to share rules across a monorepo:
extends:
- ../../shared/arklint-rules.yml
The referenced file must have the same structure as a named pack - a rules: list at the top level.
Contributing a pack
See packs/CONTRIBUTING.md for the full guide. No Python required - just YAML.
CLI Reference #
Every command and flag Arklint supports.
| Command | Description |
|---|---|
arklint init |
Generate a starter .arklint.yml in the current directory |
arklint init --force |
Overwrite an existing config file |
arklint check |
Scan the codebase from the current directory |
arklint check <path> |
Scan a specific directory |
arklint check --strict |
Exit with code 1 on warnings (not just errors) |
arklint check --quiet / -q |
Suppress passing rules - only show failures and warnings |
arklint check --json |
Machine-readable JSON output for CI pipelines |
arklint check --diff <ref> |
Only scan files changed vs a git ref |
arklint check --github-annotations |
Emit GitHub Actions inline PR annotations |
arklint check -c <path> |
Use a config file from a custom path |
arklint validate |
Validate .arklint.yml without running any checks |
arklint validate -c <path> |
Validate a config file at a custom path |
arklint search <query> |
Search official rule packs by name, description, or tag |
arklint add <pack> |
Add an official rule pack to .arklint.yml |
arklint export --format <fmt> |
Export rules as an AI assistant instruction file |
arklint export --output <dir> |
Write exported file to a specific directory |
arklint learn --describe <text> |
Generate a rule from a plain-English description using AI |
arklint learn --provider <name> |
AI provider: anthropic or openai (required) |
arklint learn --append |
Append suggested rule to .arklint.yml without prompting |
arklint visualize |
Print a Mermaid diagram of your architecture rules |
arklint visualize -o <file> |
Write diagram to a file instead of stdout |
arklint watch |
Watch for file changes and re-run checks automatically |
arklint watch --strict |
Watch mode with strict severity |
arklint mcp |
Start the MCP server (stdio) for AI agent integration |
arklint --version |
Show version and exit |
Watch Mode #
Re-run architecture checks automatically on every file save. Get instant feedback without switching to a terminal.
$ arklint watch $ arklint watch ./src $ arklint watch --strict
How it works
Watch mode uses watchdog to monitor filesystem events in your project directory. On every file change, Arklint re-runs all checks and prints the updated results.
What triggers a re-run:
- Any file save or creation in the watched directory tree
- Changes to .arklint.yml itself
What is automatically ignored:
- Hidden directories (.git, .venv, etc.)
- __pycache__, dist, build, node_modules
Press Ctrl+C to stop.
Options
| Flag | Description |
|---|---|
<path> |
Directory to watch. Defaults to current directory |
--strict |
Treat warnings as errors |
-c / --config <path> |
Use a config file from a custom path |
Typical usage
Run it once when you start coding and leave it in the background. Any violation you introduce shows up immediately.
$ arklint watch ./src --strict Watching ./src for changes… (Ctrl+C to stop) [12:34:01] Change detected - re-running checks… ✗ FAIL no-direct-db-in-routes routes/orders.py → imports 'psycopg2' - blocked [12:34:09] Change detected - re-running checks… ✓ PASS no-direct-db-in-routes ✓ PASS no-print-statements ✓ PASS layered-architecture
Diff Mode #
Scan only the files that changed - not the entire codebase. Keeps CI fast on large repositories and makes PR checks precise.
$ arklint check --diff HEAD # staged + unstaged changes vs HEAD $ arklint check --diff origin/main # files changed on your branch vs main $ arklint check --diff main --strict # fail on warnings too
How it works
Diff mode runs git diff --name-only <ref> under the hood to get the list of changed files, then restricts scanning to only those files. All rules still apply - only the file set is narrowed.
This means:
- A boundary rule only fires if a changed file has the forbidden import
- A pattern-ban rule only fires on changed files
- dependency rules always scan the full dependency file (since any change can introduce a conflict)
Why use it
On a repo with thousands of files, arklint check scans everything every run. In CI, that can be slow. With --diff origin/main, only the files touched by the pull request are scanned - which is usually the right scope anyway.
# In CI: only check what this PR changed arklint check --diff origin/main --strict --github-annotations
Options
| Flag | Description |
|---|---|
--diff <ref> |
Git ref to diff against (HEAD, origin/main, a commit SHA, etc.) |
--strict |
Treat warnings as errors |
--json |
Machine-readable JSON output |
--github-annotations |
Emit inline PR annotations (for use inside GitHub Actions) |
--diff flag is ignored if no .git directory is found.Visualize #
Generate a [Mermaid](https://mermaid.live) diagram of your architecture rules directly from `.arklint.yml`.
$ arklint visualize
Output
The command prints a flowchart LR Mermaid block to stdout. You can paste it into any Markdown file or drop it into mermaid.live to render it interactively.
For a three-layer setup the output looks like:
flowchart LR
subgraph arch_layers ["Clean layers"]
routes["routes\nroutes/**"]
services["services\nservices/**"]
repositories["repositories\nrepositories/**"]
routes --> services
routes -. blocked .-> repositories
services --> repositories
services -. blocked .-> routes
repositories -. blocked .-> routes
repositories -. blocked .-> services
end
Solid arrows (-->) are allowed dependencies. Dashed arrows (-. blocked .->) are blocked ones.
Write to a file
$ arklint visualize -o docs/architecture.md ✓ Diagram written to docs/architecture.md
You can then embed it in a Markdown page:
```mermaid
flowchart LR
...
```
GitHub and most documentation platforms render Mermaid natively inside fenced code blocks.
What gets visualised
| Rule type | Rendered as |
|---|---|
layer-boundary |
Directed graph with allowed/blocked edges per layer |
boundary |
Source directories → blocked import packages |
dependency |
“choose one” node connected to competing packages |
pattern-ban |
Not rendered (text-based, no graph structure) |
file-pattern |
Not rendered (text-based, no graph structure) |
Options
| Flag | Description |
|---|---|
-o / --output <file> |
Write diagram to a file instead of stdout |
-c / --config <path> |
Use a config file from a custom path |
MCP Server #
Expose your architectural rules to AI agents via the Model Context Protocol. AI coding tools can check code before writing it - and refuse to generate violations automatically.
$ pip install 'arklint[mcp]' $ arklint mcp
arklint mcp directly.Available tools
- list_rules - Returns all configured rules from
.arklint.ymlwith full config details. - get_rule_details - Inspect a single rule’s full configuration by its ID.
- check_file - Validate an existing file against all rules and return violations.
- check_snippet - Validate a code snippet before writing it to disk. Supports virtual paths for path-based rules.
The check_snippet tool is especially powerful: an AI agent validates code before it exists on disk. Pass a filename parameter (e.g. routes/user.py) so that path-based rules like boundary and file-pattern apply correctly.
Claude Code integration
Add to your Claude Code config (.claude/settings.json or ~/.claude/settings.json):
{
"mcpServers": {
"arklint": {
"command": "arklint",
"args": ["mcp"]
}
}
}
Once connected, Claude Code calls check_snippet automatically before writing files that match your rules.
Cursor integration
Add to your Cursor MCP config (.cursor/mcp.json):
{
"mcpServers": {
"arklint": {
"command": "arklint",
"args": ["mcp"]
}
}
}
Pair with export for best results
The MCP server handles runtime checks. For AI agents to understand your rules and avoid writing violations in the first place, also export your rules as native AI instructions:
$ arklint export --format claude # writes CLAUDE.md $ arklint export --format cursorrules # writes .cursorrules
This two-layer approach - instructions at generation time, MCP validation at write time - gives the strongest architecture enforcement with AI tools.
arklint export whenever you update your rules to keep the AI instruction files in sync.Export Rules #
Export your `.arklint.yml` rules as AI assistant instruction files so your coding tools respect the same architectural constraints.
$ arklint export --format cursorrules $ arklint export --format claude $ arklint export --format copilot
Formats
| Format | Output file | Used by |
|---|---|---|
cursorrules |
.cursorrules |
Cursor IDE |
claude |
CLAUDE.md |
Claude Code |
copilot |
.github/copilot-instructions.md |
GitHub Copilot |
Each file lists your rules with severity tags ([ERROR] / [WARN]) and descriptions in a format the AI understands natively.
Options
| Flag | Description |
|---|---|
--format / -f |
Output format (required): cursorrules, claude, or copilot |
--output / -o |
Directory to write into. Defaults to current directory |
--config / -c |
Path to .arklint.yml. Auto-discovered if omitted |
Example
$ arklint export --format claude --output .
This writes CLAUDE.md in your project root. Claude Code will automatically pick it up on the next session and respect your architectural rules when generating code.
arklint export whenever you update your rules to keep the AI instruction files in sync.AI Rule Generation #
Generate a valid `.arklint.yml` rule from a plain-English description using AI.
$ arklint learn --describe "no raw SQL queries in route handlers" --provider anthropic $ arklint learn --describe "no raw SQL queries in route handlers" --provider openai
Arklint prompts the AI with your description and the full rule schema. The AI returns a ready-to-use YAML rule block. You can review it and optionally append it to your .arklint.yml.
The anthropic and openai SDKs are bundled in the prebuilt binary, so arklint learn works out of the box regardless of whether you installed via pip, npm, or .NET. All you need is an API key.
pip install - optional extras
If you installed via pip, install the AI extra for your provider:
$ pip install 'arklint[ai-anthropic]' # Claude (Anthropic) $ pip install 'arklint[ai-openai]' # GPT-4o-mini (OpenAI)
ai-anthropic and ai-openai - with the ai- prefix. pip install arklint[anthropic] will fail. The --provider flag uses just anthropic or openai (no prefix) - a different naming from the pip extras.Options
| Flag | Description |
|---|---|
--describe / -d |
Plain-English description of the rule (required) |
--provider / -p |
AI provider: anthropic or openai (required) |
--api-key |
API key for the provider. Falls back to ANTHROPIC_API_KEY or OPENAI_API_KEY |
--append / -a |
Append the rule to .arklint.yml without confirmation prompt |
--config / -c |
Path to .arklint.yml. Auto-discovered if omitted |
Example session
$ arklint learn --describe "services must not import from routes" --provider anthropic Generating rule via anthropic… Suggested rule: - id: no-routes-in-services type: boundary description: Services must not import from route handlers source: "services/**" blocked_imports: - "routes" severity: error Append this rule to .arklint.yml? [y/N]: y ✓ Rule appended to /my-project/.arklint.yml Run arklint validate to confirm the config is valid.
--api-key flag and passed directly to the provider’s SDK at runtime.CI / Pre-commit #
Block architectural violations automatically on every push or commit.
GitHub Actions
The simplest setup - add Arklint to any existing workflow:
- name: Run Arklint
run: |
pip install arklint
arklint check --strict
For large repos, use diff mode to only scan changed files:
- name: Run Arklint (diff)
run: |
pip install arklint
arklint check --diff origin/main --strict --github-annotations
The --github-annotations flag emits inline PR annotations so violations appear directly on the changed lines in the pull request.
pre-commit hook
Add to your .pre-commit-config.yaml:
- repo: local hooks: - id: arklint name: Arklint entry: arklint check language: python pass_filenames: false
Install the hook:
$ pre-commit install
GitLab CI
arklint: stage: lint image: python:3.11-slim script: - pip install arklint - arklint check --strict
CircleCI
- run:
name: Arklint
command: |
pip install arklint
arklint check --strict
Exit codes
| Code | Meaning |
|---|---|
0 |
All checks passed |
1 |
One or more errors (or warnings with --strict) |
2 |
Config file invalid or not found |
All CI systems treat a non-zero exit code as a pipeline failure.
GitHub Action #
Use Arklint as a first-class GitHub Action with built-in diff mode, inline PR annotations, version pinning, and strict mode.
- uses: Kaushik13k/ark-lint@main with: strict: "true" diff: "origin/main"
Full example
name: Arklint on: pull_request: push: branches: [main] jobs: arklint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: Kaushik13k/ark-lint@main with: strict: "true" diff: "origin/main"
fetch-depth: 0 in actions/checkout so the full git history is available for diff mode. Without it, origin/main may not be resolvable.Inputs
| Input | Description | Default |
|---|---|---|
version |
Arklint version to install. Pin for reproducible builds. | latest |
strict |
Treat warnings as errors ("true" / "false") |
"false" |
diff |
Only scan files changed vs this ref (e.g. origin/main) |
(all files) |
config |
Path to .arklint.yml. Auto-discovered if omitted. |
(auto) |
working-directory |
Directory to run arklint in, for monorepos | . |
Prerequisites
The action requires a .arklint.yml in your repository root. If it’s missing you’ll see:
Config error: No .arklint.yml found. Run 'arklint init' to create one.
Run this locally first and commit the file:
arklint init git add .arklint.yml git commit -m "chore: add arklint config" git push
If your config lives elsewhere (e.g. a monorepo), point the action to it explicitly:
- uses: Kaushik13k/ark-lint@main with: config: "services/api/.arklint.yml"
Inline PR annotations
When diff mode is enabled, the action automatically passes --github-annotations to emit inline annotations on the pull request. Violations appear directly on the changed lines - no need to scroll through logs.
Language Support #
Arklint is language-agnostic. Import extraction and dependency file parsing are built in for the most common languages and package managers - no plugins or configuration needed.
Import extraction
Arklint reads import statements from source files to enforce boundary and layer-boundary rules.
| Language | File extensions | Import syntax detected |
|---|---|---|
| Python | .py |
import x, from x import y |
| JavaScript | .js, .mjs, .cjs |
import, require() |
| TypeScript | .ts, .tsx |
import, require() |
| Go | .go |
import "pkg", import ( ... ) |
| Ruby | .rb |
require, require_relative |
| Rust | .rs |
use crate::, extern crate |
| Java | .java |
import com.example.Class |
| C# | .cs |
using Namespace |
| PHP | .php |
use, require, include |
Dependency file parsing
Arklint reads dependency files to enforce dependency rules (e.g. “keep only one HTTP client”).
| Package manager | File |
|---|---|
| Python (pip) | requirements.txt |
| Python (modern) | pyproject.toml |
| Node.js | package.json |
| Go | go.mod |
| Rust | Cargo.toml |
| Ruby | Gemfile |
Language detection
Arklint detects language from file extension automatically. No configuration needed - rules based on file paths (like source: "routes/**") match regardless of the file’s language.
Contributing #
Arklint is open source and welcomes contributions. Every change goes through a pull request.
Dev setup
$ git clone https://github.com/Kaushik13k/arklint $ cd arklint $ python -m venv .venv && source .venv/bin/activate $ pip install -e ".[dev]" $ pytest -v
Ground rules: every PR needs CODEOWNER approval, every source change needs a test, and all CI checks must pass. See CONTRIBUTING.md for the full guide.