Skip to content
sarobPublic

About

Deterministic codebase intelligence for Python — dead code, duplication, complexity, architecture boundaries, and quality scoring.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

hallow

Deterministic codebase intelligence for Python.

Dead code, duplication, circular dependencies, complexity scoring, architecture boundaries — one tool, zero config, machine-readable output.

Inspired by fallow (JS/TS), adapted for Python's import system, packaging, and dynamic features.

Status

Phase Scope Status
Phase 1 Foundation + Dead Code Complete
Phase 2 Complexity + Health + Duplication Complete
Phase 3 Boundaries + Plugins + Fix Complete
Phase 4 Output Formats + Audit + CI Complete
Phase 5 Security + MCP + Polish Complete

Install

pip install hallow

Or from source:

git clone https://github.com/sarob/hallow.git
cd hallow
pip install -e ".[dev]"

Usage

# Full scan — dead code + duplication + health
$ hallow

# Dead code only
$ hallow check --format sarif --output findings.sarif

# Health score
$ hallow health --score
  Health: 82/100 (B)
  Complexity hotspots: 3
  Maintainability: 71.4 avg

# Duplicate detection
$ hallow dupes --mode mild --min-lines 8

# PR gate — fail only on findings this change introduces
$ hallow audit                      # base: merge-base with upstream / origin/main / main
$ hallow audit --base origin/main --gate all

# Auto-fix
$ hallow fix --dry-run
  Would remove: 4 unused imports, 2 dead files, 1 unused dep

# Architecture boundary check
$ hallow check --boundaries layered

# Security scan
$ hallow security

# MCP server
$ hallow mcp --stdio

Programmatic API

from hallow.api import detect_dead_code, detect_circular_dependencies, compute_complexity

# Dead code analysis
results = detect_dead_code(root="./my-project")
for finding in results.findings:
    print(f"{finding.location.file}:{finding.location.line} {finding.message}")

# Circular imports
cycles = detect_circular_dependencies(root="./my-project")
for cycle in cycles:
    print(" -> ".join(cycle["modules"]))

# Complexity metrics
for func in compute_complexity(root="./my-project"):
    if func["cyclomatic"] > 10:
        print(f"{func['file']}:{func['name']} cc={func['cyclomatic']}")

Why Hallow

No single tool covers Python the way Fallow covers JS/TS. Today you need vulture for dead code, radon for complexity, import-linter for circular deps, pylint for duplication — each with its own config, output format, and no awareness of the others. None detect unused files, unused dependencies, or offer project-level quality scoring.

Hallow runs all analyses through one pipeline, one config, one data model:

Discover → Extract → Resolve → Graph → Analyze → Report

It uses Python's built-in ast module — no external parser — and produces unified output in JSON, SARIF, Markdown, or Rich ANSI.

Architecture

The source is organised as these modules and packages, one per pipeline stage or surface:

Module Role
hallow.types Pydantic models: ModuleInfo, Finding, AnalysisResults, Severity. Pure schema, no logic.
hallow.config Config from .hallowrc.toml or pyproject.toml [tool.hallow]. Zero-config defaults.
hallow.extract AST extraction via ast.parse(). Produces ModuleInfo per file: imports, exports, __all__, complexity.
hallow.graph Module graph from ModuleInfo. Import resolution, reachability, cycle detection (Tarjan's SCC).
hallow.core Analysis orchestration. One pipeline runs the detectors in sequence: dead code, unused deps, circular imports and boundaries read the module graph; duplication reads the token stream.
hallow.security Hardcoded-secret and taint-sink detectors.
hallow.plugins Framework awareness. Auto-activated from declared dependencies: Django, Flask, FastAPI, pytest, Celery.
hallow.output Formatters: JSON, SARIF, Markdown, CodeClimate, GitHub annotations, compact, human (Rich ANSI).
hallow.cli Typer CLI: hallow check, health, dupes, audit, fix, security, mcp.
hallow.mcp MCP server. 6 tools for agent-driven codebase intelligence.

Features

Dead code detection

  • Unused files (unreachable from entry points)
  • Unused imports, functions, classes, variables
  • Unused dependencies (declared in pyproject.toml, or requirements.txt when there is no [project] table, but never imported)
  • Unlisted dependencies (imported but not declared)
  • __all__ entries that name nothing the module defines or imports

Complexity & health

  • Per-function cyclomatic + cognitive complexity
  • Project-level health score (0-100, A-F grade)
  • Maintainability index
  • Complexity hotspot identification

Duplication

  • Clone detection by hashing fixed-size token windows and extending each match to its longest common run
  • Four modes, matching fallow's: strict and mild (exact tokens; comments and whitespace are never tokens), weak (string values ignored), semantic (identifiers, strings and numbers ignored, so renamed copies match)
  • Upgrading from 0.1.0: mild (the default) used to ignore identifiers and literals, and weak also ignored operators. The default now reports exact copies only. To keep reporting renamed copies, set mode = "semantic" under [tool.hallow.duplicates] (or [duplicates] in .hallowrc.toml)

Circular dependencies

  • Detection via Tarjan's strongly connected components
  • Full cycle chain visualization

Architecture boundaries

  • Layered, hexagonal, feature-sliced presets
  • Custom zone definitions with import rules
  • Namespace package awareness

Framework plugins

  • Auto-detected from declared dependencies (pyproject.toml or requirements*.txt)
  • Django, Flask, FastAPI, pytest, Celery
  • Suppresses false positives for framework-registered exports (decorators, entry files, migrations)

Auto-fix

  • hallow fix --dry-run previews safe changes
  • hallow fix --apply removes unused imports, deletes dead files, strips unused deps from pyproject.toml or requirements*.txt

Security

  • Hardcoded secrets (API keys, tokens, passwords, private keys, JWTs)
  • Taint sink detection (eval, exec, os.system, subprocess, pickle, SQL injection)
  • Safe-context filtering (env vars, config lookups, placeholders)

Output formats

  • Human (Rich ANSI), JSON, compact
  • SARIF v2.1.0 (GitHub code scanning, VS Code)
  • Markdown (PR comments and reports)
  • CodeClimate JSON (GitLab Code Quality)
  • GitHub Actions annotations (::error/::warning)

CI integration

  • This repository's own CI (.github/workflows/ci.yml): pytest on Python 3.11–3.14 with an 80% line-coverage floor, ruff, and hallow check on its own source
  • Pre-commit hook configuration
  • Exit-code gating: exit 1 on error-severity findings; --ci = SARIF + --fail-on-issues + --quiet
  • hallow audit: pass/warn/fail on the findings a change introduces, against a base re-analysed in a temporary worktree (no baseline file needed)
  • hallow check --changed-since <ref>: report only findings in changed files
  • --fail-on-baseline-growth: fail when a committed baseline gains entries
  • Baseline files (--save-baseline / --baseline) for regression prevention

MCP server

  • JSON-RPC 2.0 over stdio — hallow mcp --stdio
  • 6 tools: analyze, health, complexity, cycles, duplicates, explain
  • Agent-driven codebase intelligence via Model Context Protocol

Configuration

Zero config by default. Customize via .hallowrc.toml or pyproject.toml:

# .hallowrc.toml
entry = ["src/main.py"]
ignore_patterns = ["**/migrations/**"]

[rules]
unused-files = "error"      # error (default), warn, off
unused-imports = "error"
unused-variables = "warn"
circular-dependencies = "error"
high-complexity = "warn"

[duplicates]
mode = "mild"               # strict, mild, weak, semantic
min_tokens = 50
min_lines = 5

[health]
max_cyclomatic = 20
max_cognitive = 15

Or in pyproject.toml:

[tool.hallow]
entry = ["src/main.py"]

[tool.hallow.rules]
unused-files = "error"
unused-imports = "warn"

Suppressing findings

Suppress a finding in the source, optionally with a reason after --:

import plugin_hooks  # hallow-ignore unused-imports -- registers hooks on import


# hallow-ignore-next-line unused-functions -- called by name from the job runner
def nightly_rollup(): ...


# hallow-ignore-file unused-files

hallow-ignore applies to its own line, hallow-ignore-next-line to the next line of code, and hallow-ignore-file to the whole file. Rule names are comma- or space-separated; with none, every rule is suppressed. A suppression that no longer matches a finding is reported as stale-suppression, and so is one naming an unknown rule. A stale-suppression finding can itself be suppressed by another comment, never by the comment it reports on. # noqa / # noqa: F401 still suppresses unused imports.

Gating a change: hallow audit

$ hallow audit
Audit scope: 3 changed file(s) vs merge-base with origin/main (4c1d9e2a7b10)

app/payments.py
  E new unused-imports 're' (import re) is unused app/payments.py:4
  W inherited high-complexity 'charge' has cyclomatic complexity 24 (max 20) app/payments.py:88

✗ fail: 1 finding(s) gated · 1 inherited excluded (run with --gate all to enforce)

hallow audit analyses the working tree, keeps the findings that touch a changed file (committed, staged, unstaged or untracked), then re-analyses the base commit in a temporary git worktree with the same config and compares line-free finding keys:

  • a finding whose key exists at the base is inherited; otherwise it is introduced. Moving code keeps a finding inherited, and renamed files are mapped old → new, so a pure git mv stays inherited
  • a complexity finding is introduced when the function is new or the metric it exceeds went up
  • an introduced clone group none of whose fragments contains an added line is demoted to inherited
  • a change touching no Python file, dependency file or hallow config skips the base analysis

Scope limits: audit gates findings located in changed files, so a finding a change causes in an unchanged file (deleting the last caller makes an untouched function unused) is not gated — run hallow check for that. A rename across the --root boundary is not mapped. With --baseline, entries are spent on inherited occurrences before introduced ones.

The verdict is fail (exit 1) if a gated finding is error-severity, warn (exit 0) if one is warn-severity, else pass. --gate new-only (default) gates introduced findings; --gate all gates every finding in changed files and skips the base analysis.

The base is --base <ref> (alias --changed-since), else $HALLOW_AUDIT_BASE, else the merge-base of HEAD with the branch's upstream, origin/HEAD, origin/main, origin/master, main or master; with none, audit exits 2. In CI, check out with full history (fetch-depth: 0). The JSON document (kind: "audit") carries verdict, base_ref, merge_base, changed_files, per-category attribution and introduced on every finding; --format sarif|github|... emits only the gated findings.

Finding IDs and baselines

Every finding carries a finding_id such as hl1:unused-functions:81a349a3b9ea3b15. It is a hash of the rule, the file path and the symbol, never the line, so moving code keeps the id; renaming the file or the symbol changes it. Repeats of the same key get ~1, ~2, … in line order. SARIF output carries it as a partialFingerprints entry and CodeClimate output as its fingerprint.

Baselines record findings by the same line-free key:

hallow check --save-baseline hallow-baseline.json   # once
hallow check --baseline hallow-baseline.json        # later runs report only new findings
hallow baselines prune --baseline hallow-baseline.json [--check]

--fail-on-baseline-growth (with --baseline) fails when the committed baseline file holds an entry its version at --baseline-base <ref> (default: the audit base detection) lacks — so a baseline can only shrink. A baseline stores one entry per occurrence, so N saved entries hide at most N current findings. The JSON output's baseline_staleness reports how many entries matched; when a quarter or more match nothing, a warning goes to stderr. baselines prune removes entries that no current finding matches and never adds any (--check changes nothing and exits 1 if there is something to prune). A file that is not a hallow check baseline — including the line-keyed format earlier versions wrote — suppresses nothing and is reported as unrecognised; re-save it.

Exit codes and JSON output

Exit code Meaning
0 No enforced gate failed: no error-severity findings
1 An enforced gate failed: error-severity findings (warn-severity too with --fail-on-issues), or a stale baseline with --fail-on-stale-baseline
2 Invalid input or runtime error: bad flag value, unreadable config, unknown format
3 A requested resource is unavailable: the --config file does not exist

check, security and bare hallow exit 1 on error-severity findings without any flag. --ci is --format sarif --fail-on-issues --quiet; an explicit --format wins. --quiet silences status messages on stderr, never the output. Treat 0 and 1 as completed runs; do not hide 2 or 3 with || true.

Scripts and agents should use --format json. Each command writes one document with:

  • kind (check, security, health, dupes, baselines-prune), schema_version, version, elapsed_ms
  • gate_outcomes — each gate's status (pass/warn/fail/skipped) and whether it is enforced; the run exits 1 when an enforced gate fails
  • next_steps[] — {id, command, reason}, read-only commands, shell-quoted and carrying the run's --root/--config, that run as written from any directory
  • per finding: finding_id, rule, severity, location, symbol and actions[] ({type, auto_fixable, description, ...}; auto_fixable says whether hallow fix applies it)

An error raised while a command runs (unreadable config, unwritable --output, unknown --format, a refused baseline) is, under --format json, a document on stdout rather than a traceback: {"error": true, "message": "...", "exit_code": 2, "code": "..."}. Command-line usage errors caught by the argument parser (an unknown option, an invalid --boundaries value, options placed before the command) exit 2 with a text message on stderr.

hallow schema prints every command, flag, output format, exit code and rule as JSON. hallow schema --output-schema prints the JSON Schema of every document, committed as docs/output-schema.json.

Python-Specific Design

Hallow is not a port of Fallow — it's built for Python's idioms:

  • ast.parse() over OXC — Python's built-in AST is fast, complete, and zero-dependency. Handles all 3.11+ syntax.
  • Import resolution — relative imports, namespace packages (PEP 420, no __init__.py), conditional imports (try/except), function-level imports.
  • No explicit exports — Python uses __all__ (optional) and convention (leading underscore). Hallow treats all public module-level names as exports unless __all__ narrows the surface.
  • Dynamic features — getattr(), **kwargs, metaclasses, decorator registries. Framework plugins handle common patterns; suppression comments handle the rest.
  • pyproject.toml over package.json — dependency parsing from [project.dependencies], [project.optional-dependencies], requirements.txt fallback.
  • Parallelism via ProcessPoolExecutor — Python's GIL makes threading insufficient for CPU-bound AST parsing, so files are parsed in worker processes when there are 50 or more (extract_modules_parallel).

Stack

  • Python 3.11+ — tomllib, StrEnum, ExceptionGroup
  • Typer — typed CLI with Rich integration
  • Pydantic v2 — config validation, result schemas, JSON serialization
  • Rich — ANSI terminal output
  • Hatch — build backend
  • pytest — test suite

Development

git clone https://github.com/sarob/hallow.git
cd hallow
uv venv && uv pip install -e ".[dev]"

uv run pytest              # run tests
uv run ruff check .        # lint
uv run ruff format .       # format
uv run hallow check --root .  # self-analysis

License

Apache License 2.0 — see LICENSE for details.

About

Deterministic codebase intelligence for Python — dead code, duplication, complexity, architecture boundaries, and quality scoring.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages