DiffGuard

March 23, 2026 · View on GitHub

LLM-powered security review for staged git diffs

DiffGuard catches security vulnerabilities before they reach your codebase. It analyzes your staged git changes using OpenAI, building precise AST-aware context around each diff hunk via tree-sitter, and returns actionable findings aligned with OWASP and CWE standards.

Features

  • AST-aware context — tree-sitter parses your code to extract enclosing scopes, imports, and symbol definitions, giving the LLM far more context than raw diffs alone
  • 14 languages — full AST support for Python, JavaScript, TypeScript, Java, Ruby, Go, PHP, and Elixir; hybrid support for Vue, Svelte, and Astro SFCs; analysis-only support for HTML templates, CSS, and Makefiles
  • Framework detection — automatically recognizes Django, Flask, FastAPI, Rails, Sinatra, Laravel, WordPress, Phoenix, and more, applying framework-specific import resolution and exclusion rules
  • Deterministic severity — a built-in mapping of 295 CWEs to severity levels ensures the same vulnerability always gets the same severity, regardless of LLM output variance
  • Configurable thresholds — control which severity levels block, warn, or are allowed via .diffguard.toml
  • Baseline suppression — mark known false positives so they never appear again
  • Sensitive file protection — 112+ built-in patterns automatically prevent .env files, private keys, certificates, and cloud credentials from being sent to the LLM
  • JSON reports — machine-readable output with --json or --output
  • Dry-run mode — preview which files and how many tokens would be analyzed without making API calls

Quick Start

Get up and running in under a minute:

# 1. Install DiffGuard
git clone https://github.com/nick79/diffguard.git
cd diffguard
uv tool install .

# 2. Set your OpenAI API key
export OPENAI_API_KEY=your-key-here

# 3. Navigate to any project, stage changes, and scan
cd /path/to/your/project
git add -p
diffguard

DiffGuard exits with code 0 if no blocking issues are found, or 1 if it detects vulnerabilities that exceed your severity thresholds.

Installation

Requirements

Install from source

git clone https://github.com/nick79/diffguard.git
cd diffguard
uv tool install .

This installs diffguard as a globally available command. The uv tool install command creates an isolated virtual environment, installs all dependencies, and makes the diffguard CLI available system-wide.

Setting up your API key

DiffGuard requires an OpenAI API key set as an environment variable. Add it to your shell profile for persistence:

# ~/.zshrc, ~/.bashrc, or equivalent
export OPENAI_API_KEY=your-key-here

DiffGuard reads the key from the OPENAI_API_KEY environment variable at runtime. It does not read from .env files — you must export the variable in your shell.

Updating DiffGuard

When a new version is available:

cd /path/to/diffguard
git pull
uv tool uninstall diffguard
uv cache clean diffguard
uv tool install .

Usage

Navigate to any git repository, stage your changes, and run diffguard:

# Run security analysis on staged changes
diffguard

# Preview what would be analyzed (no API calls)
diffguard --dry-run

# Output findings as JSON to stdout
diffguard --json

# Save JSON report to a file
diffguard --output report.json

# Verbose mode with timing, token counts, and per-file status
diffguard --verbose

# Combine flags
diffguard --verbose --dry-run
diffguard --json --output report.json

CLI Flags

FlagShortDescription
--verbose-vShow timing, file count, token estimates, and per-file analysis status
--dry-runList files and estimate tokens without calling the LLM
--jsonOutput findings as JSON to stdout (suppresses terminal formatting)
--output FILESave JSON report to the specified file path
--versionPrint the DiffGuard version and exit
--helpShow all available commands and flags

Exit Codes

CodeMeaning
0Pass — no blocking findings (warn/allow findings may still be printed)
1Block — findings above severity threshold exist (commit should be rejected)
2Error — not a git repo, missing API key, config error, LLM failure, etc.
130Interrupted — user pressed Ctrl+C

Pre-commit Hook

Add DiffGuard as a git pre-commit hook to automatically scan staged changes before every commit:

cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
diffguard
EOF
chmod +x .git/hooks/pre-commit

How it works: Git runs the pre-commit hook before creating each commit. If the hook script exits with a non-zero code, git aborts the commit. DiffGuard already exits with code 1 when it detects blocking findings (Critical or High severity by default), so no additional scripting is needed — just calling diffguard is enough to block commits with security issues. Exit code 0 (no blocking findings) lets the commit proceed normally.

To bypass the hook for a specific commit (e.g., after reviewing findings and accepting the risk), use git commit --no-verify.

Configuration

DiffGuard works out of the box with sensible defaults — no configuration file is required. To customize settings for a specific project, create a file named .diffguard.toml in the root directory of the project you want to scan (the same directory where your .git/ folder lives). DiffGuard searches for this file starting from the current working directory and walking up the directory tree, so you can also place it in a parent directory to apply settings to multiple projects.

Only the settings you want to change need to be included — all others keep their defaults. Below is the full reference showing every available option with its default value:

# LLM model to use for analysis
model = "gpt-5.2"  # default

# Number of lines to expand around each changed hunk for context
hunk_expansion_lines = 50  # default

# Maximum lines for scope extraction before truncation
scope_size_limit = 200  # default

# How deep to follow imports for symbol resolution
symbol_resolution_depth = 1  # default

# LLM sampling temperature (0 = deterministic, higher = more creative)
temperature = 0.0  # default

# Minimum confidence level for findings ("Low", "Medium", "High")
# Set to "Medium" to filter out borderline findings and improve run-to-run consistency
min_confidence = "Low"  # default (keep all findings)

# Maximum total tokens per scan (0 = unlimited)
max_tokens_per_scan = 0  # default (unlimited)

# Maximum concurrent API calls
max_concurrent_api_calls = 5  # default

# API timeout in seconds
timeout = 120  # default

# Abort on LLM errors in non-interactive (piped) mode
# Set to false to skip failed files and continue
fail_on_error = true  # default

# Patterns identifying third-party code paths (excluded from analysis and symbol resolution)
third_party_patterns = [
    "venv/", ".venv/", "site-packages/",
    "node_modules/", "bower_components/",
    "target/", "build/", ".gradle/",
    "vendor/bundle/", "vendor/ruby/", ".bundle/",
    "tmp/cache/", "log/"
]  # default

# Path to baseline file (relative to project root)
baseline_path = ".diffguard-baseline.json"  # default

# Additional glob patterns for sensitive file exclusion
# These are added on top of the built-in defaults (.env, *.pem, *.key, etc.)
sensitive_patterns = []  # default

# Whether to include the built-in sensitive file patterns
use_default_sensitive_patterns = true  # default

# Severity thresholds: action per severity level
# Actions: "block" (exit 1), "warn" (print warning, exit 0), "allow" (no output)
[thresholds]
critical = "block"   # default
high = "block"       # default
medium = "warn"      # default
low = "allow"        # default
info = "allow"       # default

Environment Variables

VariableDescription
OPENAI_API_KEYYour OpenAI API key (required for analysis, not needed for --dry-run)

Sensitive File Exclusion

DiffGuard automatically excludes files matching known secrets patterns from being sent to the LLM. This provides defense-in-depth even if sensitive files are accidentally staged.

Default patterns include:

  • Environment files: .env, .env.*, *.env
  • Private keys: *.pem, *.key, id_rsa, id_ed25519, etc.
  • Certificates: *.crt, *.p12, *.pfx, *.jks
  • Secrets files: secrets.json, credentials.json, secrets.yaml, etc.
  • Cloud credentials: .aws/credentials, service-account*.json, etc.
  • Infrastructure: *.tfvars, *.tfstate, etc.

The two settings work together:

use_default_sensitive_patternssensitive_patternsResult
true (default)empty (default)Built-in patterns only
true["*.custom"]Built-in + your custom patterns
false["*.custom"]Your custom patterns only
falseemptyNo exclusion (not recommended)

Built-in patterns are enabled by default for security. To extend them, add your patterns to sensitive_patterns — they are appended to the built-in list. To take full control, set use_default_sensitive_patterns = false and provide your own complete list.

Severity Thresholds

Control what happens when findings of each severity level are detected:

ActionBehavior
blockFinding causes exit code 1 (blocks commit in pre-commit hook)
warnFinding is printed with a warning, but does not block
allowFinding is included in output but does not block

Defaults: Critical and High block, Medium warns, Low and Info are allowed. Customize in .diffguard.toml:

[thresholds]
medium = "block"   # upgrade Medium to blocking
high = "warn"      # downgrade High to warning-only

Only the levels you specify are overridden — unmentioned levels keep their defaults.

Severity Classification

DiffGuard uses a two-stage approach to classify finding severity:

  1. LLM identifies the vulnerability — the OpenAI model detects security issues and assigns a CWE identifier (e.g., CWE-89 for SQL Injection).
  2. Deterministic severity mapping — a built-in CWE-to-severity mapping (295 CWEs) assigns the final severity in code, ensuring the same CWE always gets the same severity regardless of LLM output variance.

This hybrid approach combines the LLM's ability to find vulnerabilities with deterministic, standards-based severity assignment. The same codebase will always produce the same severity breakdown across runs.

Severity levels

LevelDescriptionExamples
CriticalUnauthenticated RCE, command/code injection, complete auth bypassOS command injection (CWE-78), eval injection (CWE-95), SSTI (CWE-1336)
HighExploitable with significant impact, may require some conditionsSQL injection (CWE-89), XSS (CWE-79), path traversal (CWE-22), SSRF (CWE-918)
MediumConditional exploitation or moderate impactCSRF (CWE-352), information disclosure (CWE-200), open redirect (CWE-601)
LowMinor issues with minimal security impactVerbose error messages (CWE-209), debug code (CWE-489), obsolete functions
InfoBest practice recommendations, no direct exploitabilityCode quality, insufficient logging, coding standards

Override rules

  • Low confidence cap: If the LLM has low confidence in a finding, severity is capped at Medium — preventing high-severity alerts on uncertain detections.
  • Unmapped CWEs: For CWEs not in the built-in map, the LLM's suggested severity is used as a fallback.

Sources

The built-in CWE-to-severity mapping covers 295 CWEs compiled from:

Baseline Management

Suppress known false positives so they don't appear in future scans. Baselined findings are automatically excluded from scan results, exit code evaluation, and output. If any findings are suppressed, the summary shows the count (e.g., "2 issues found (1 suppressed)").

Adding a finding to the baseline

After a scan, use the finding ID shown in the output:

diffguard baseline add cwe89-a1b2c3d4e5f6a7b8 --reason "Validated input upstream"

Bulk-adding Low/Info findings

diffguard baseline add --all-low

Removing a finding from the baseline

diffguard baseline remove cwe89-a1b2c3d4e5f6a7b8

Listing baselined findings

diffguard baseline list

The baseline file defaults to .diffguard-baseline.json in the project root. Override with:

baseline_path = "custom/path/baseline.json"

Supported Languages

DiffGuard uses tree-sitter for AST parsing to build precise context around code changes. Full language support includes scope detection, import extraction, symbol resolution, and first-party/third-party code classification.

LanguageExtensionsAST Support
Python.py, .pyiFull
JavaScript.js, .mjs, .cjs, .jsxFull
TypeScript.ts, .mts, .cts, .tsxFull
Java.javaFull
Ruby.rbFull
Go.goFull
PHP.phpFull
Elixir.ex, .exsFull
Vue.vueHybrid (script: Full, template: Analysis-only)
Svelte.svelteHybrid (script: Full, template: Analysis-only)
Astro.astroHybrid (frontmatter: Full, template: Analysis-only)
HTML & Templates.html, .htm, .ejs, .hbs, .handlebars, .njk, .nunjucks, .pug, .erb, .heex, .jinja, .jinja2, .mustache, .blade.phpAnalysis-only
CSS & Stylesheets.css, .scss, .sass, .lessAnalysis-only
MakefileMakefile, makefile, GNUmakefile, .mkAnalysis-only

Files with unsupported or unrecognized extensions are skipped. Binary files are automatically excluded.

AST support levels:

  • Full — tree-sitter parsing with scope detection, import extraction, symbol resolution, and first-party/third-party classification
  • Hybrid — script/frontmatter blocks receive full AST enrichment; template/markup sections receive analysis-only treatment
  • Analysis-only — files are included in the diff context with raw hunk expansion but do not receive AST-based enrichment

Python

Scope detection: Functions, async functions, classes, methods, nested definitions. Decorators are included in scope boundaries. Lambdas and comprehensions are not treated as scopes.

Symbol resolution: When a changed code region references a symbol imported from a first-party module, DiffGuard resolves the import to its source file and includes the symbol's definition in the LLM context. This gives the LLM visibility into helper functions, base classes, and utilities that the changed code depends on.

First-party detection: Only first-party (project-local) symbols are resolved — third-party and stdlib code is excluded:

  • Relative imports (from .module import X) are always first-party
  • Standard library modules (os, json, pathlib, etc.) are excluded
  • Modules whose resolved file path matches a third_party_patterns entry are excluded

Third-party code patterns: The third_party_patterns config controls which paths are excluded from analysis and symbol resolution. Files under these paths are skipped entirely in the pipeline (never sent to the LLM) and are also excluded from symbol resolution. Default patterns for Python:

  • venv/ — standard virtual environment
  • .venv/ — common virtual environment alternative
  • site-packages/ — installed packages

These patterns are matched against file paths. Staged files under these directories are skipped during analysis, and symbols resolving to these directories are not included in the LLM context.

Module resolution: DiffGuard resolves Python imports to file paths using standard conventions:

  • module.py — single-file modules
  • module/__init__.py — package modules
  • src/module.py and src/module/__init__.py — src layout projects

Generated file detection: Generated Python files are automatically excluded from analysis:

  • Django migrations: files under */migrations/*.py with # Generated by Django header
  • Generic auto-generated: files with # Auto-generated, # Generated by, or # DO NOT EDIT in the first 5 lines

Python Framework Support

Django: DiffGuard detects Django projects (via manage.py at project root) and adds Django app awareness for first-party detection. App directories containing __init__.py plus conventional files (models.py, views.py, admin.py, forms.py, urls.py, apps.py, serializers.py) are recognized as first-party modules even when they can't be resolved via standard import paths.

Additional excluded paths: staticfiles/ (collectstatic output), .tox/ (test runner cache), htmlcov/ (coverage reports)

Flask: DiffGuard detects Flask projects (via Flask(__name__) in app.py, wsgi.py, or application.py). Flask uses standard Python imports throughout, so no special resolution is needed beyond detection.

FastAPI: DiffGuard detects FastAPI projects (via FastAPI() in main.py, app.py, or asgi.py). FastAPI uses standard Python imports throughout, so no special resolution is needed beyond detection.

JavaScript

Scope detection: Functions, arrow functions, function expressions, generator functions, async functions, classes, and methods. Arrow functions and function expressions assigned to variables inherit the variable name. Anonymous callbacks get <anonymous>.

Symbol resolution: When a changed code region references a symbol imported from a first-party module, DiffGuard resolves the import to its source file. Both ES6 imports (import { x } from './utils') and CommonJS (const x = require('./utils')) are supported. Dynamic import() expressions are also detected.

First-party detection: Only first-party (project-local) symbols are resolved — third-party packages are excluded:

  • Relative imports (./utils, ../lib) are always first-party
  • Bare specifiers matching the name field or workspaces entries in package.json are first-party
  • All other bare specifiers (e.g., lodash, express) are treated as third-party

Third-party code patterns: Default patterns for JavaScript:

  • node_modules/ — npm/yarn installed packages
  • bower_components/ — Bower installed packages

Generated file detection: Minified and bundled JavaScript files are automatically excluded from analysis:

  • Filename patterns: .min.js, .min.mjs, .min.cjs, .bundle.js, .chunk.js
  • Content heuristic: files with average line length > 500 characters

Module resolution: DiffGuard resolves JavaScript imports using standard Node.js conventions:

  • ./module.js — exact file path
  • ./module — tries .js, .mjs, .cjs, .jsx extensions
  • ./lib — tries index.js in directory (index convention)

TypeScript

Scope detection: Typed functions, arrow functions with type annotations, function expressions, generator functions, async functions, classes (including generics), methods, and namespaces. Extends JavaScript scope detection patterns with TypeScript-specific constructs.

Symbol resolution: When a changed code region references a symbol imported from a first-party module, DiffGuard resolves the import to its source file. Both ES6 imports and CommonJS require() calls are supported. Type-only imports (import type { ... }) and inline type imports (import { type Foo, bar }) are recognized and excluded from runtime symbol resolution — only value imports are resolved.

First-party detection: Same rules as JavaScript — relative imports are first-party, bare specifiers are checked against package.json name/workspaces:

  • Relative imports (./utils, ../lib) are always first-party
  • Bare specifiers matching the name field or workspaces entries in package.json are first-party
  • All other bare specifiers (e.g., lodash, @types/node) are treated as third-party

Third-party code patterns: Same as JavaScript:

  • node_modules/ — npm/yarn installed packages
  • bower_components/ — Bower installed packages

Generated file detection: Generated TypeScript files are automatically excluded from analysis:

  • Declaration files (.d.ts, .d.mts, .d.cts) with generated headers (// Generated by, // Auto-generated) in the first 5 lines
  • Content heuristic: files with average line length > 500 characters (minified/bundled)
  • Note: .d.ts files in node_modules/ are already skipped by vendor path filtering

Module resolution: DiffGuard resolves TypeScript imports using standard conventions:

  • ./module.ts — exact file path
  • ./module — tries .ts, .tsx, .mts, .cts, .js, .mjs, .cjs, .jsx extensions
  • ./lib — tries index.ts/index.tsx/etc. in directory (index convention)

Java

Scope detection: Classes, interfaces, enums, methods, constructors, static methods, inner classes, and lambdas. Annotations (e.g., @Override, @Service) are included in scope boundaries as part of the method/class declaration.

Symbol resolution: When a changed code region references a symbol imported from a first-party package, DiffGuard resolves the import to its source file. Both regular imports (import com.example.Helper;) and static imports (import static com.example.Utils.format;) are supported. Wildcard imports (import com.example.*;) are also detected.

First-party detection: Only first-party (project-local) symbols are resolved — standard library and third-party code is excluded:

  • Standard library packages (java.*, javax.*, jdk.*, com.sun.*) are excluded
  • Project base package is detected from pom.xml (<groupId>), build.gradle/build.gradle.kts (group), or inferred from src/main/java/ directory structure
  • Imports matching the project base package are first-party
  • Imports resolvable to files under src/main/java/ or src/test/java/ are first-party

Third-party/build output paths: Default patterns for Java:

  • target/ — Maven build output
  • build/ — Gradle build output
  • .gradle/ — Gradle cache directory

Generated file detection: Generated Java files are automatically excluded from analysis:

  • Path patterns: **/generated-sources/**, **/generated/**, **/apt_generated/** (annotation processor output)
  • Content heuristic: files with @Generated annotation in the first 20 lines

Module resolution: DiffGuard resolves Java imports using standard Maven/Gradle conventions:

  • com.example.MyClasssrc/main/java/com/example/MyClass.java
  • Also tries src/test/java/ and src/ layouts

Ruby

Scope detection: Methods (def), class methods (def self.method), classes, modules, blocks (do...end / { }), and lambdas (->). Nested modules and classes are correctly resolved to the innermost scope.

Symbol resolution: When a changed code region references a symbol imported via require or require_relative, DiffGuard resolves the import to its source file. Ruby's snake_case-to-CamelCase naming convention is used to match class names to file names (e.g., require 'my_helper' resolves symbol MyHelper).

First-party detection: Only first-party (project-local) symbols are resolved — standard library and third-party gems are excluded:

  • require_relative imports are always first-party
  • Standard library modules (json, net/http, fileutils, etc.) are excluded
  • Gems listed in Gemfile are treated as third-party
  • require calls that resolve to a local file (under lib/, app/, or project root) are first-party

Third-party/vendor paths: Default patterns for Ruby:

  • vendor/bundle/ — Bundler gem install location
  • vendor/ruby/ — Alternative Bundler path
  • .bundle/ — Bundler metadata

Generated file detection: Generated Ruby files are automatically excluded from analysis:

  • db/schema.rb — Rails auto-generated schema dump
  • Content heuristic: files with # This file is auto-generated, # Generated by, or # DO NOT EDIT in the first 5 lines

Module resolution: DiffGuard resolves Ruby imports using standard conventions:

  • require_relative './helper' — relative to current file, tries .rb extension
  • require 'my_app/helper' — tries lib/my_app/helper.rb, my_app/helper.rb, app/my_app/helper.rb

Ruby Framework Support

Rails: DiffGuard detects Rails projects (via config/application.rb) and adds Zeitwerk-style autoload resolution. When a changed region references a class with no explicit require, DiffGuard resolves it via Rails conventions:

  • Userapp/models/user.rb
  • UsersControllerapp/controllers/users_controller.rb
  • Admin::DashboardControllerapp/controllers/admin/dashboard_controller.rb
  • UserMailerapp/mailers/user_mailer.rb
  • UserJobapp/jobs/user_job.rb

Additional excluded paths for Rails projects: tmp/cache/, log/

Additional generated file detection: db/migrate/*.rb files with # This migration was auto-generated header

Sinatra / Padrino: Fully supported via base Ruby support — route blocks are captured as block scopes, and explicit require statements are handled by the standard Ruby import extraction.

Go

Scope detection: Functions, methods (value and pointer receivers), anonymous functions (goroutines), and init functions. Go does not have classes — struct types are defined at package level and are not treated as scopes.

Symbol resolution: When a changed code region references a symbol imported from a first-party package, DiffGuard resolves the import to its source file. Both single imports (import "fmt") and grouped imports (import (...)) are supported, including aliased imports (import f "fmt"), blank/side-effect imports (import _ "database/sql"), and dot imports (import . "strings").

First-party detection: Only first-party (project-local) symbols are resolved — standard library and third-party code is excluded:

  • Standard library packages (no dots in import path: fmt, net/http, crypto/tls) are excluded
  • The module path is read from go.mod (e.g., module github.com/myorg/myapp)
  • Imports prefixed with the module path are first-party
  • All other imports (with dots but not matching module path) are third-party

Third-party/vendor paths: Default patterns for Go:

  • vendor/ — Go modules vendor directory

Generated file detection: Generated Go files are automatically excluded from analysis:

  • // Code generated ... DO NOT EDIT. convention (the standard go generate header, as first content line after optional build tags)
  • Filename patterns: *.pb.go (protobuf), *_string.go (stringer), mock_*.go/*_mock.go (mock generators), *_gen.go (general generated suffix)

Module resolution: DiffGuard resolves Go imports using go.mod module path:

  • github.com/myorg/myapp/internal/utilsinternal/utils/*.go (first non-test .go file in the package directory)

PHP

Scope detection: Functions, methods (public/private/protected/static), classes, traits, interfaces, anonymous functions (closures with use clause), and arrow functions (PHP 7.4+). Constructors (__construct) are detected as methods.

Symbol resolution: When a changed code region references a symbol imported via use statements, DiffGuard resolves the import to its source file using PSR-4 autoload conventions. Both use class imports and grouped use statements (use App\Models\{User, Post}) are supported. require/include and their _once variants are also detected.

First-party detection: Only first-party (project-local) symbols are resolved — third-party packages are excluded:

  • use statements matching PSR-4 namespace prefixes from composer.json are first-party
  • require/include with relative paths (./, ../, __DIR__) are first-party
  • All other use statements (e.g., Symfony\..., Laravel\...) are treated as third-party

Third-party/vendor paths: Default patterns for PHP:

  • vendor/ — Composer installed packages

Generated/cache file detection: Generated and cached PHP files are automatically excluded from analysis:

  • Path patterns: var/cache/ (Symfony), bootstrap/cache/ (Laravel), storage/framework/cache/ (Laravel)
  • Content heuristic: files with <?php // auto-generated, @generated, or similar markers in the first 5 lines

Module resolution: DiffGuard resolves PHP imports using PSR-4 conventions from composer.json:

  • App\Services\UserService with {"App\\": "src/"}src/Services/UserService.php
  • Also tries src/, app/, lib/ directories as fallbacks

Laravel Framework Support

Laravel: DiffGuard detects Laravel projects (via artisan file at project root) and adds convention-based symbol resolution. When a changed region references a class with no matching import, DiffGuard resolves it via Laravel's App\app/ directory convention:

  • App\Models\Userapp/Models/User.php
  • App\Http\Controllers\UserControllerapp/Http/Controllers/UserController.php
  • App\Services\Payment\StripeServiceapp/Services/Payment/StripeService.php

First-party detection: In Laravel projects, all classes under the App\ namespace are treated as first-party, even without composer.json PSR-4 configuration. Third-party packages (e.g., Illuminate\..., Laravel\...) remain excluded.

Compiled Blade views excluded: storage/framework/views/*.php (compiled Blade templates) are detected as generated files.

Additional excluded path: storage/framework/ (compiled views, cache, sessions)

Blade templates: .blade.php files are analyzed as HTML templates (analysis-only via HTML & Templates support), not as PHP files.

WordPress Framework Support

WordPress: DiffGuard detects WordPress projects via wp-config.php at the project root, or by recognizing standalone plugin/theme projects (Plugin Name: header in a PHP file, or Theme Name: header in style.css).

First-party detection: In a WordPress plugin or theme, the plugin/theme's own files are treated as first-party. WordPress core directories (wp-includes/, wp-admin/) and other plugins' files are treated as third-party.

Excluded paths: The following WordPress directories are excluded from analysis by default:

  • wp-includes/ — WordPress core
  • wp-admin/ — WordPress admin core
  • wp-content/cache/ — cache plugin output (WP Super Cache, W3 Total Cache)
  • wp-content/uploads/ — media uploads directory
  • wp-content/upgrade/ — upgrade working directory

Elixir

Scope detection: Modules (defmodule), public functions (def), private functions (defp), macros (defmacro/defmacrop), guards (defguard/defguardp), protocols (defprotocol), protocol implementations (defimpl), and anonymous functions (fn ... end).

Import extraction: All four Elixir module directives are extracted:

  • alias MyApp.Accounts.User — module alias (including multi-alias syntax alias MyApp.{User, Role})
  • import Ecto.Query — function import (with optional only: filter)
  • require Logger — compile-time require
  • use GenServer — macro-based use

Symbol resolution: When a changed code region references a module, DiffGuard resolves it to its source file using Elixir's convention-based module-to-file mapping: MyApp.Accounts.Userlib/my_app/accounts/user.ex. Aliased module names are resolved via the corresponding alias directive. Umbrella projects (apps/*/lib/) are also checked.

First-party detection: Only first-party (project-local) modules are resolved — standard library and third-party code is excluded:

  • Elixir/Erlang stdlib modules (Enum, GenServer, :crypto, :ets, etc.) are excluded
  • The app name and dependencies are read from mix.exs
  • Modules matching the project namespace (derived from app: :my_appMyApp) are first-party
  • Modules matching dependency names from the deps function are third-party

Third-party/vendor paths: Default patterns for Elixir:

  • deps/ — Mix dependency directory
  • _build/ — Build artifacts

Generated file detection: Auto-generated Elixir files are excluded from analysis:

  • Content markers: # Generated by, # DO NOT EDIT in the first 5 lines
  • Content heuristic: files with average line length > 500 characters

Elixir Framework Support

Phoenix detection: Projects are identified as Phoenix applications when {:phoenix, ...} is found in the mix.exs deps.

Directory convention resolution: Phoenix *Web modules are resolved to their conventional file locations:

  • MyAppWeb.PageControllerlib/my_app_web/controllers/page_controller.ex
  • MyAppWeb.UserLive.Indexlib/my_app_web/live/user_live/index.ex
  • MyAppWeb.CoreComponentslib/my_app_web/components/core_components.ex
  • MyAppWeb.Routerlib/my_app_web/router.ex

LiveView: Fully supported via base Elixir support. LiveView modules are standard .ex files — no special handling needed.

HEEx templates: .heex files are analyzed as HTML (analysis-only) with XSS-focused analysis. Key XSS surfaces:

  • raw/1 helper — bypasses HTML escaping
  • Phoenix.HTML.raw/1 in templates
  • Dynamic attribute injection in HEEx components

First-party detection: In Phoenix projects, both MyApp.* and MyAppWeb.* namespaces are first-party.

Excluded paths: priv/static/ (compiled assets — esbuild output, CSS bundles).

Vue

Hybrid support. Vue Single File Components (.vue) receive a hybrid analysis approach:

  • <script> block → Full AST enrichment. The script block is extracted, parsed as JavaScript or TypeScript (based on lang attribute), and receives full scope detection, import extraction, and symbol resolution. Scope line numbers are mapped back to full-file coordinates so the LLM sees correct positions.
  • <template> block → Analysis-only. The template markup is included in the expanded region for raw LLM analysis — no template-specific AST parsing.
  • <script lang="ts"> → TypeScript parsing. When the script tag specifies lang="ts" or lang="typescript", the block is parsed with the TypeScript grammar.
  • <script setup> → Supported. Composition API script setup blocks are extracted and parsed normally.

Key XSS surfaces detected by the LLM:

  • v-html="userInput" — raw HTML rendering (bypasses Vue's default escaping)
  • Dynamic :is with user-controlled values — component injection
  • Unescaped slot content in SSR contexts

Generated file detection: Minified Vue SFCs (average line length > 500 characters) are automatically excluded from analysis.

Svelte

Hybrid support. Svelte Single File Components (.svelte) receive a hybrid analysis approach:

  • <script> block → Full AST enrichment. The script block is extracted, parsed as JavaScript or TypeScript (based on lang attribute), and receives full scope detection, import extraction, and symbol resolution. Scope line numbers are mapped back to full-file coordinates so the LLM sees correct positions.
  • Template content → Analysis-only. Everything outside <script> and <style> blocks is template markup, included in the expanded region for raw LLM analysis — no template-specific AST parsing.
  • <script lang="ts"> → TypeScript parsing. When the script tag specifies lang="ts" or lang="typescript", the block is parsed with the TypeScript grammar.
  • <script context="module"> → Supported. Module-level script blocks are extracted and parsed normally.

Key XSS surfaces detected by the LLM:

  • {@html userInput} — raw HTML rendering (bypasses Svelte's default escaping)

Generated file detection: Minified Svelte components (average line length > 500 characters) are automatically excluded from analysis.

Astro

Hybrid support. Astro components (.astro) receive a hybrid analysis approach:

  • Frontmatter → Full AST enrichment. The frontmatter block (code between --- fences) is extracted, parsed as JavaScript or TypeScript, and receives full scope detection, import extraction, and symbol resolution. Scope line numbers are mapped back to full-file coordinates so the LLM sees correct positions.
  • Template content → Analysis-only. Everything after the closing --- fence is template markup, included in the expanded region for raw LLM analysis — no template-specific AST parsing.
  • TypeScript detection: Frontmatter is detected as TypeScript when it contains import type, triple-slash /// <reference types="..."> directives, or type annotations.

Key XSS surfaces detected by the LLM:

  • set:html={userInput} — raw HTML rendering (bypasses Astro's default escaping)
  • Unescaped {expressions} in template markup
  • define:vars on <script> tags — injects server-side variables into client-side code

Generated file detection: Minified Astro components (average line length > 500 characters) are automatically excluded from analysis.

HTML & Templates

Analysis-only support. HTML and common template file formats are detected and sent to the LLM for security analysis with raw hunk expansion. No AST-based scope detection, import extraction, or symbol resolution is performed.

Extensions: .html, .htm, .ejs, .hbs, .handlebars, .njk, .nunjucks, .pug, .erb, .heex, .jinja, .jinja2, .mustache, .blade.php

Blade templates: Files ending in .blade.php are detected as HTML templates (analysis-only), not as PHP files. This ensures Blade templates get XSS-focused analysis rather than full PHP AST parsing. Regular .php files are unaffected.

Key XSS surfaces detected by the LLM:

  • EJS: <%- userInput %> (unescaped output)
  • Jinja2/Nunjucks: {{ var | safe }}, {% autoescape false %}
  • Handlebars/Mustache: {{{ raw }}} (triple-brace unescaped)
  • Pug: !{userInput} (unescaped interpolation)
  • ERB: <%= raw_html %> combined with html_safe
  • HEEx: raw/1 helper, Phoenix.HTML.raw/1 (bypass HTML escaping)
  • Blade: {!! $variable !!} (unescaped), @php blocks

Generated file detection: Minified HTML files (average line length > 500 characters) are automatically excluded from analysis.

CSS & Stylesheets

Analysis-only support. CSS and preprocessor files are detected and sent to the LLM for security analysis with raw hunk expansion. No AST-based scope detection, import extraction, or symbol resolution is performed.

Extensions: .css, .scss, .sass, .less

Key security surfaces detected by the LLM:

  • CSS injection via url() for data exfiltration
  • @import SSRF (loading external stylesheets from attacker-controlled URLs)
  • Legacy IE vectors: expression(), behavior: (CSS-based code execution)
  • Sensitive data leakage via attribute selectors combined with url()

Makefile

Analysis-only support. Makefiles are detected by filename (Makefile, makefile, GNUmakefile) or extension (.mk) and included in diff analysis with raw hunk expansion. No AST-based scope detection, import extraction, or symbol resolution is performed — the LLM analyzes the raw code context directly.

Examples

Basic pre-commit usage

# Stage specific files and scan them
git add src/auth.py src/api/handlers.py
diffguard

# If DiffGuard finds issues, fix them and re-scan
vim src/auth.py
git add src/auth.py
diffguard

Custom severity thresholds

# .diffguard.toml — strict mode for security-critical services
[thresholds]
critical = "block"
high = "block"
medium = "block"   # upgrade Medium to blocking
low = "warn"       # show Low findings as warnings
info = "allow"
# .diffguard.toml — relaxed mode for internal tools
[thresholds]
critical = "block"
high = "warn"      # downgrade High to warning-only
medium = "allow"
low = "allow"
info = "allow"

JSON output for downstream processing

# Pipe findings to jq for filtering
diffguard --json | jq '.findings[] | select(.severity == "Critical")'

# Save report and check summary
diffguard --output report.json
cat report.json | jq '.summary'

Dry-run to estimate cost

# See how many tokens would be sent before making API calls
diffguard --dry-run

Output includes per-file token estimates and a total, so you can gauge API cost before running the full scan.

Troubleshooting

"Not a git repository"

Error: Not a git repository. Run diffguard from within a git project.

DiffGuard must be run from inside a git repository. Make sure you're in the correct directory:

cd /path/to/your/project
git status  # verify it's a git repo
diffguard

"No staged changes to analyze"

No staged changes to analyze. Use git add to stage files first.

DiffGuard only analyzes staged (not committed) changes. Stage your files first:

git add -p          # interactively stage hunks
# or
git add src/file.py # stage a specific file
diffguard

"OPENAI_API_KEY not set"

Error: OPENAI_API_KEY environment variable is not set.

Export your API key in your shell:

export OPENAI_API_KEY=your-key-here

Add it to your shell profile (~/.zshrc, ~/.bashrc) for persistence. Use --dry-run to preview analysis without an API key.

LLM timeout or rate limit errors

Error: Request timed out. Try again or increase the timeout.
Error: Rate limit exceeded. Wait a moment and try again.
  • Timeout: Increase the timeout in .diffguard.toml: timeout = 300
  • Rate limit: Wait a few seconds and retry. Reduce max_concurrent_api_calls to lower the request rate.
  • Large diffs: Use max_tokens_per_scan to cap total token usage, or stage fewer files at a time.

Invalid API key

Error: Invalid API key. Check your OPENAI_API_KEY.

Verify your key is correct and has not expired. Check at platform.openai.com/api-keys.

Config file errors

Error: Failed to parse .diffguard.toml — invalid TOML syntax.

Validate your TOML syntax. Common mistakes:

  • Missing quotes around string values (e.g., model = gpt-5.2 instead of model = "gpt-5.2")
  • Unclosed brackets in table headers
  • Invalid table headers (e.g., [threshold] instead of [thresholds])

Unsupported file types skipped

Files with unrecognized extensions (.xyz, .custom, etc.) are silently skipped. This is expected — DiffGuard only analyzes known source file types. See the Supported Languages table for the full list.

High token usage

If scans are consuming too many tokens:

  • Set max_tokens_per_scan to cap total usage per scan
  • Reduce hunk_expansion_lines to send less context per file (default: 50)
  • Reduce scope_size_limit to truncate large scope extractions (default: 200)
  • Stage fewer files at a time

Server errors (500)

Error: Server error. The OpenAI API returned an internal error. Try again later.

This is an OpenAI-side issue. Wait a moment and retry. If the problem persists, check OpenAI status.

Development

# Install dependencies
uv sync

# Run all tests (1600+ tests)
uv run pytest

# Run with verbose output
uv run pytest -v

# Run with coverage
uv run pytest --cov=src

# Run only integration tests
uv run pytest tests/integration/

# Format code
uv run ruff format .

# Lint code
uv run ruff check .

# Auto-fix lint issues
uv run ruff check . --fix

# Type check (strict mode)
uv run mypy .

License

PolyForm Noncommercial License 1.0.0 — see LICENSE for details.