Skip to content

diff-contract

Deterministic guardrails for AI-generated diffs — define what files can change, block violations.

AI coding tools (Cursor, Claude Code, Codex) sometimes modify unrelated files or drift outside the scope of the task. diff-contract sits between the generated code and your repository and enforces deterministic constraints — plain glob rules in a YAML file, no second AI pass reviewing the first one. It reads the file list of a diff, matches each path against your rules, and exits non-zero if anything is out of bounds.

I want to…

I want to… Do this
Check my branch against a contract diff-contract check
Check specific files, no git needed diff-contract validate --files
Write a contract from scratch diff-contract init
Understand a violation message Rules — how deny, allow and limits interact
Gate a pull request The shipped GitHub Action — see CI
Publish findings to Code Scanning --sarif — see CI
Block a commit locally Pre-commit hook — see CI
Use it from Python See the API Reference

Install

pip install diff-contract

Requires Python 3.10 or newer. The only runtime dependency is PyYAML.

Sixty-second run

Write a 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

Then check the current branch against main:

diff-contract check
✗ 2 violation(s):
  🔴 [BLOCK] File 'src/core/x.py' is denied by rule 'Block core changes'
  🔴 [BLOCK] File 'src/core/x.py' not allowed by rule 'Allow feature X'

Exit code 1 means a violation blocked the diff; 2 means warnings only.

How it decides

For every changed file, all rules are evaluated. A file that matches a rule's deny globs is a violation; a file that matches none of a rule's allow globs — when that rule has any — is also a violation. Deny wins: if any deny matched, only deny violations are reported for that file.

Path matching is Python's fnmatch, not gitignore syntax. * crosses directory separators, so src/* and src/** behave identically and both match src/a/b/c.py. Matching is case-sensitive on Linux and macOS.

Beyond per-file rules, max_files and max_lines cap the size of the whole diff — see Rules.

Where to go next

  • Getting Started — install and first run
  • Usage — every flag of check, validate and init
  • Rules — glob semantics, deny precedence, aggregate limits
  • Configuration — every key in .diffcontract.yml
  • CI — GitHub Action, Code Scanning, pre-commit hook
  • API Reference — the Python API
  • Example contracts — ready-to-copy files for Python, React, Django, Rust and docs-only projects