Adopt one Python quality loop across a repository or an organization:
pipx install interlocks # or: uv tool install interlocks
cd your-python-project
interlocks doctor # readiness, detected config, blockers, next steps
interlocks check # local edit loop
interlocks ci # CI parityinterlocks bundles ruff, basedpyright, pytest, pytest-bdd, coverage, mutmut, deptry, import-linter, pip-audit, and lizard behind one CLI. New repositories can start with auto-detected paths and bundled tool defaults; mature repositories can opt into named presets or explicit [tool.interlocks] thresholds when they need stronger gates.
pipx install interlocks
# or
uv tool install interlocksEvery underlying tool ships with the CLI. No per-project dev dependency list is required just to try the standard loop.
cd your-python-project
interlocks doctordoctor is the safe first command. It performs static local inspection only: nearest pyproject.toml, detected source/test/features paths, runner, invoker, active preset, resolved gate values, PATH visibility, blockers, warnings, and shortest next steps. It does not run tests, typecheck, coverage, mutation, dependency audit, or network checks.
If the repository is ready, doctor points you at interlocks check and CI wiring. If it is blocked, it prioritizes the minimum setup fixes first, such as interlocks init, missing paths, unreadable config, unsupported presets, or missing runnable tool resolution.
interlocks checkcheck runs the local edit loop: fix, format, typecheck, tests, optional acceptance tests, advisory dependency hygiene, cached CRAP feedback when fresh coverage exists, and the suppressions report. It is the command to run after edits before pushing.
The direct CI command is:
interlocks ciFor GitHub Actions, copy this workflow:
name: interlocks
on:
pull_request:
push:
branches: [main]
jobs:
interlocks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: 0xjgv/interlocks@v1The reusable action installs interlocks, runs interlocks ci, and writes a concise GITHUB_STEP_SUMMARY when GitHub provides the summary file. The action does not duplicate lint, typecheck, coverage, CRAP, dependency, architecture, acceptance, or mutation logic; the CLI remains the source of truth.
When agents write most of the PRs, human review stops being the quality floor. Deterministic gates become the part that scales:
crapcatches complex code the agent shipped without matching tests.mutationcatches tests the agent wrote that do not actually test the code.coverageand complexity trends feed drift telemetry — signal that agent output is regressing before users notice.trustcombines those into one actionable report, so reviewers (human or LLM-based) have a stable ground truth.
interlocks is complementary to LLM-based reviewers such as CodeRabbit, Greptile, or Diamond. They catch style, design, and intent. interlocks catches what is machine-verifiable: complexity, coverage, mutation survival, dependency hygiene, architectural drift. Runs in seconds, same command locally and in CI.
Presets are optional defaults under [tool.interlocks]. Explicit values in the same layer override preset defaults, so you can manually tune thresholds in pyproject.toml after choosing a preset.
[tool.interlocks]
preset = "baseline" # "baseline" | "strict" | "legacy"baselinelowers first-adoption friction: advisory CRAP, relaxed thresholds, mutation off in CI, acceptance off incheck.strictis for mature repositories: stronger thresholds, blocking CRAP and mutation, mutation in CI, acceptance incheck.legacyis for ratcheting existing repositories: very permissive thresholds, advisory gates, mutation off in CI.
agent-safe is intentionally unsupported. If configured, interlocks doctor reports it as an unsupported preset instead of resolving agent-specific defaults.
Nothing is required. interlocks walks up from CWD to the nearest pyproject.toml and auto-detects:
- project root: first directory with
pyproject.toml - test runner: pytest if pytest config/deps/imports are present, otherwise unittest
- test dir: first existing of
tests/,test/,src/tests/ - source dir: build-backend declarations, package layouts,
src/<pkg>, top-level packages, or the project root - test invoker:
uv runwhenuv.lockexists, elsepython -m - features dir: first existing of
tests/features/,features/,<test_dir>/features/
Override anything via [tool.interlocks] in pyproject.toml:
[tool.interlocks]
preset = "baseline"
# Paths / runners
src_dir = "mypkg"
test_dir = "tests"
test_runner = "pytest" # "pytest" | "unittest"
test_invoker = "python" # "python" | "uv"
pytest_args = ["-q", "-x"]
# Thresholds
coverage_min = 80
crap_max = 30.0
complexity_max_ccn = 15
complexity_max_args = 7
complexity_max_loc = 100
mutation_min_coverage = 70.0
mutation_max_runtime = 600
mutation_min_score = 80.0
# Gate behavior
enforce_crap = true
run_mutation_in_ci = false
enforce_mutation = false
mutation_ci_mode = "off" # "off" | "incremental" | "full"
mutation_since_ref = "origin/main"
# Acceptance
acceptance_runner = "pytest-bdd" # "pytest-bdd" | "behave" | "off"
features_dir = "tests/features"
run_acceptance_in_check = falsePrecedence, lowest to highest:
- Bundled dataclass defaults.
- Project preset defaults from
[tool.interlocks]. - Project explicit values.
- CLI flags inside tasks, such as
--min=,--max=,--max-runtime=,--min-score=, and--min-coverage=.
Run interlocks help to see the active preset and resolved values.
Run interlocks presets to see preset options, their main thresholds, and copyable config.
Run interlocks presets set baseline to set a project preset from the CLI.
| Stage | When | What runs |
|---|---|---|
interlocks check |
Local edit loop | fix -> format -> parallel(typecheck, test, acceptance when opted in) -> deps advisory -> cached CRAP advisory or refresh hint -> suppressions |
interlocks pre-commit |
Git pre-commit hook | fix/format staged Python files, re-stage, typecheck, tests when source changed |
interlocks ci |
Pull requests and protected branches | format-check, lint, complexity, deps, typecheck, coverage, arch, acceptance -> CRAP -> optional mutation |
interlocks nightly |
Scheduled jobs | coverage -> mutation, always blocking on mutation_min_score |
interlocks post-edit |
Editor/agent hook interface | advisory ruff fix + format on changed Python files |
interlocks setup-hooks |
Convenience installer | writes hooks that call interlocks pre-commit and interlocks post-edit |
interlocks clean |
Local cleanup | removes caches, build artifacts, coverage output, mutation state, and __pycache__/ |
interlocks pre-commit and interlocks post-edit are the stable hook interfaces. interlocks setup-hooks is a convenience command that installs a git pre-commit hook and merges a Claude Code Stop hook; rerunning it is idempotent.
Correctness:
fix/format: ruff lint-fix and format, mutating files.lint/format-check: read-only equivalents for CI.typecheck: basedpyright.test: pytest or unittest, auto-detected.acceptance: Gherkin via pytest-bdd or behave.
Hygiene:
audit: pip-audit CVE scan.deps: deptry unused, missing, and transitive import checks.arch: import-linter contracts; default contract forbids source importing tests.
Advanced gates:
coverage --min=N: coverage.py with fail-under.--min=Noverridescoverage_min.crap --max=N [--changed-only]: CRAP complexity x coverage gate. Blocking depends onenforce_crap.mutation --max-runtime=N [--min-coverage=N] [--min-score=N] [--changed-only]: mutmut. Advisory unlessenforce_mutation = trueor--min-score=is passed.trust [--refresh] [--no-trend]: actionable trust report combining coverage, CRAP, mutation, suspicious-test AST inspection, recent git diff, and next actions.--refreshruns coverage first with--min=0.
Scaffolding:
init: writes a greenfieldpyproject.toml,tests/__init__.py, andtests/test_smoke.py; refuses to overwrite.init-acceptance: writes a working pytest-bdd example undertests/features/andtests/step_defs/; refuses to overwrite.
Utility:
config: list every[tool.interlocks]key with type, default, description, and current resolved value (read-only). Single source of truth for agents driving setup.doctor: adoption diagnostic. Exempt from thepyproject.tomlpreflight gate.help: command list plus detected paths, active preset, and thresholds.presets: show preset options, current values, copyable config, and set a project preset withinterlocks presets set <preset>.version: print the installed interlocks version.
Drop .feature files under tests/features/ and step definitions under tests/step_defs/; interlocks acceptance runs them via pytest-bdd and shares coverage with test. Or run interlocks init-acceptance for a working example.
Runner detection order:
acceptance_runnerin config ("pytest-bdd","behave", or"off").- Behave layout:
features_dir/steps/plusfeatures_dir/environment.py. behavedeclared as a dependency but notpytest-bdd.- Default to pytest-bdd.
Acceptance always runs in interlocks ci when a features directory exists. It is opt-in for interlocks check via run_acceptance_in_check = true.
When the target project has no config for a given tool, interlocks injects its bundled default.
| File | Consumed by | Detected via | Injected flag |
|---|---|---|---|
ruff.toml |
fix, format, lint, format-check |
[tool.ruff], ruff.toml, .ruff.toml |
--config |
pyrightconfig.json |
typecheck |
[tool.basedpyright], pyrightconfig.{json,toml} |
--project |
coveragerc |
coverage |
[tool.coverage.*], .coveragerc |
--rcfile= |
importlinter_template.ini |
arch |
[tool.importlinter], .importlinter, setup.cfg |
formatted tempfile plus --config |
bdd_example.feature |
init-acceptance |
none | direct copy |
bdd_test_example.py |
init-acceptance |
none | direct copy |
bdd_conftest.py |
init-acceptance |
none | direct copy |
scaffold_pyproject.toml |
init |
none | read plus {project_name} substitution |
scaffold_test_example.py |
init |
none | direct copy |
interlocks deps and interlocks mutation ship no bundled fallback: deptry applies its built-ins, and mutmut reads the project's pyproject.toml.
Use the stable hook interfaces directly when integrating with your own hook manager:
interlocks pre-commit
interlocks post-editUse the convenience installer when you want interlocks to write the common hooks:
interlocks setup-hooksIt installs:
.git/hooks/pre-commit: runsinterlocks pre-commit. Skip withgit commit --no-verifywhen necessary..claude/settings.jsonStop hook: runsinterlocks post-editafter Claude Code sessions and preserves existing Stop hooks.
Both reference the Python that installed interlocks, so reinstall hooks after switching install locations or interpreters.
Package identity:
- PyPI distribution:
interlocks - import package:
interlocks - CLI command:
interlocks
Trusted Publishing setup:
- PyPI: owner
0xjgv, repointerlocks, workflowrelease.yml, environmentpypi - TestPyPI: owner
0xjgv, repointerlocks, workflowrelease.yml, environmenttestpypi - No PyPI API token required.
Release checklist:
- Set
pyproject.tomlversion to the next release. - Set
interlocks/__init__.py__version__to the same release. - Update
CHANGELOG.mdfor the release. - Run
uv run interlocks ci. - Run
uv build. - Trigger
releasemanually to publish to TestPyPI. - Create matching
vX.Y.Ztag. - Push tag.
- Confirm PyPI release, GitHub release assets, and attestations.
See CHANGELOG.md for release history.