diff-contract
Deterministic guardrails for AI-generated diffs — define what files can change, block violations.
pip install diff-contract
diff-contract check --contract .diffcontract.yml
The Problem
AI coding tools (Cursor, Claude Code, Codex) sometimes modify unrelated files, introduce changes outside the intended scope, or drift from the original structure. diff-contract sits between AI-generated code and your repo, enforcing deterministic constraints — not relying on another AI pass to review.
"Most tools either help generate code or review it after the fact, but there's no real control layer in between." — HN discussion, 2026
Quick Start
1. Install
pip install diff-contract
2. Define your contract
# .diffcontract.yml
version: 1
rules:
- name: "Block core changes"
deny:
- "src/core/**"
- "*.env"
on_violation: block
- name: "Allow feature X"
allow:
- "src/features/X/**"
- "tests/features/X/**"
on_violation: block
3. Check your diff
# Check current branch vs main
diff-contract check
# Check specific files
diff-contract check --files src/app.py src/utils.py
# JSON output (for CI)
diff-contract check --output json
4. GitHub Action
# .github/workflows/diff-contract.yml
name: diff-contract
on: pull_request
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: yunaremaia/diff-contract@main
Validate files (no git required)
Validate specific files against your contract without a git diff — ideal for pre-commit hooks:
diff-contract validate --files src/app.py tests/test_app.py
echo "src/foo.py" | diff-contract validate --from-stdin
Initialize a contract
Create a starter .diffcontract.yml:
diff-contract init # default Python project contract
diff-contract init --template react # React/Next.js
diff-contract init --template django # Django
diff-contract init --template rust # Rust workspace
diff-contract init --template docs # documentation-only
Ready-to-use templates for React, Django, Rust, and documentation-only projects are available in the examples/ directory.
Pre-commit hook
diff-contract ships a pre-commit hook. Add to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/yunaremaia/diff-contract
rev: v0.1.0
hooks:
- id: diff-contract
args: ["--contract", ".diffcontract.yml"]
SARIF Output (GitHub Code Scanning)
Generate SARIF 2.1.0 output for GitHub Code Scanning integration:
diff-contract check --sarif > diff-contract.sarif
diff-contract validate --files src/foo.py --sarif
GitHub Actions workflow:
- uses: yunaremaia/diff-contract@main
with:
format: sarif
sarif-output: diff-contract.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: diff-contract.sarif
SARIF output includes one rule per violation type. Block violations emit at error level, warnings at warning.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Clean — no violations |
| 1 | Block violation — file denied, outside allowed scope, or an aggregate limit exceeded with on_violation: block |
| 2 | Warning — non-blocking violation (e.g., large diff with on_violation: warn) |
Rules
- allow: File globs that are permitted (all others blocked)
- deny: File globs that are denied (takes priority)
- on_violation:
block(exit 1) orwarn(exit 2) - max_files / max_lines: optional aggregate size limits (see below)
Aggregate Limits
Path allow/deny rules constrain which files may change. max_files and max_lines constrain how large a change set may be. They are evaluated against the whole diff after per-file rules run.
| Field | Meaning |
|---|---|
max_files |
Maximum number of changed files in the diff |
max_lines |
Maximum total changed lines (added + deleted from git diff --numstat) |
A rule may set either field, both, or neither. Limits are omitted by default (no size budget). When a limit is exceeded, the engine records an aggregate violation on the synthetic path <aggregate> and applies that rule's on_violation:
on_violation: block— fail the check (exit code 1). Use this to stop AI agents or CI from landing oversized diffs.on_violation: warn— report the overshoot but continue (exit code 2 if there are no block violations). Use this as a PR-size nudge.
Typical uses:
- Cap feature work so an agent cannot rewrite half the tree while implementing one ticket.
- Keep documentation or bugfix rules tight even when the path globs are broad.
- Warn on large diffs without blocking hotfixes.
Limits are compared against the entire change list, not only files that match that rule's allow/deny globs. A rule that only sets max_files / max_lines (no path patterns) is a global size guard.
Example
# .diffcontract.yml
version: 1
rules:
- name: "Block core changes"
deny:
- "src/core/**"
- "*.env"
on_violation: block
- name: "Feature development"
allow:
- "src/features/**"
- "tests/features/**"
max_files: 15
max_lines: 400
on_violation: block
- name: "Large diff warning"
max_files: 20
max_lines: 600
on_violation: warn
In this contract:
- Changes under
src/core/**or*.envare blocked. - Feature-area diffs may proceed only if they stay within 15 files and 400 lines; exceeding either budget is a block (exit 1).
- Any diff larger than 20 files or 600 lines also produces a warning (exit 2 when nothing is blocked).
See examples/strict.yml for a fuller contract that combines deny rules with per-rule size budgets.
License
MIT
diff-contract
Metadata
Release files for diff-contract 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| diff_contract-0.1.1.tar.gz | 17.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| diff_contract-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 29.9 kB
Release files / diff_contract-0.1.1.tar.gz
| Download URL | diff_contract-0.1.1.tar.gz |
|---|---|
| Size | 17.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d865e67e8d639b0206a68f084aca780dd4f8ca1568f3173ac5311b4988ce3179
|
|
BLAKE2b-256 checksum How to use checksums |
85c757960a42d4344bf296d81711dfcf564dea32762e3981fb0b25fc9c7efed9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / diff_contract-0.1.1-py3-none-any.whl
| Download URL | diff_contract-0.1.1-py3-none-any.whl |
|---|---|
| Size | 12.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5d475c61307a663c5fb8240eebb739e4375576218c4dd82fd7b3873d19841ce4
|
|
BLAKE2b-256 checksum How to use checksums |
022916378dc5623aec195eb8cbc7ac2dcb88bd5e38a7f1147d4cb2a50e39ad8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log