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
.envfiles, private keys, certificates, and cloud credentials from being sent to the LLM - JSON reports — machine-readable output with
--jsonor--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
- Python 3.14+
- uv (Python package manager)
- An OpenAI API key
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
| Flag | Short | Description |
|---|---|---|
--verbose | -v | Show timing, file count, token estimates, and per-file analysis status |
--dry-run | List files and estimate tokens without calling the LLM | |
--json | Output findings as JSON to stdout (suppresses terminal formatting) | |
--output FILE | Save JSON report to the specified file path | |
--version | Print the DiffGuard version and exit | |
--help | Show all available commands and flags |
Exit Codes
| Code | Meaning |
|---|---|
0 | Pass — no blocking findings (warn/allow findings may still be printed) |
1 | Block — findings above severity threshold exist (commit should be rejected) |
2 | Error — not a git repo, missing API key, config error, LLM failure, etc. |
130 | Interrupted — 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
| Variable | Description |
|---|---|
OPENAI_API_KEY | Your 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_patterns | sensitive_patterns | Result |
|---|---|---|
true (default) | empty (default) | Built-in patterns only |
true | ["*.custom"] | Built-in + your custom patterns |
false | ["*.custom"] | Your custom patterns only |
false | empty | No 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:
| Action | Behavior |
|---|---|
block | Finding causes exit code 1 (blocks commit in pre-commit hook) |
warn | Finding is printed with a warning, but does not block |
allow | Finding 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:
- LLM identifies the vulnerability — the OpenAI model detects security issues and assigns a CWE identifier (e.g., CWE-89 for SQL Injection).
- 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
| Level | Description | Examples |
|---|---|---|
| Critical | Unauthenticated RCE, command/code injection, complete auth bypass | OS command injection (CWE-78), eval injection (CWE-95), SSTI (CWE-1336) |
| High | Exploitable with significant impact, may require some conditions | SQL injection (CWE-89), XSS (CWE-79), path traversal (CWE-22), SSRF (CWE-918) |
| Medium | Conditional exploitation or moderate impact | CSRF (CWE-352), information disclosure (CWE-200), open redirect (CWE-601) |
| Low | Minor issues with minimal security impact | Verbose error messages (CWE-209), debug code (CWE-489), obsolete functions |
| Info | Best practice recommendations, no direct exploitability | Code 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:
- MITRE CWE Top 25 (2024/2025) — most dangerous software weaknesses
- OWASP Top 10 (2021) — web application security risks
- SANS Top 25 — most dangerous software errors
- CWE-1003 Simplified Mapping — commonly mapped CWEs in published vulnerabilities
- SAST tool coverage from SonarQube, Semgrep, CodeQL, Checkmarx, Fortify, GitLab SAST, and Mend SAST
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.
| Language | Extensions | AST Support |
|---|---|---|
| Python | .py, .pyi | Full |
| JavaScript | .js, .mjs, .cjs, .jsx | Full |
| TypeScript | .ts, .mts, .cts, .tsx | Full |
| Java | .java | Full |
| Ruby | .rb | Full |
| Go | .go | Full |
| PHP | .php | Full |
| Elixir | .ex, .exs | Full |
| Vue | .vue | Hybrid (script: Full, template: Analysis-only) |
| Svelte | .svelte | Hybrid (script: Full, template: Analysis-only) |
| Astro | .astro | Hybrid (frontmatter: Full, template: Analysis-only) |
| HTML & Templates | .html, .htm, .ejs, .hbs, .handlebars, .njk, .nunjucks, .pug, .erb, .heex, .jinja, .jinja2, .mustache, .blade.php | Analysis-only |
| CSS & Stylesheets | .css, .scss, .sass, .less | Analysis-only |
| Makefile | Makefile, makefile, GNUmakefile, .mk | Analysis-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_patternsentry 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 alternativesite-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 modulesmodule/__init__.py— package modulessrc/module.pyandsrc/module/__init__.py— src layout projects
Generated file detection: Generated Python files are automatically excluded from analysis:
- Django migrations: files under
*/migrations/*.pywith# Generated by Djangoheader - Generic auto-generated: files with
# Auto-generated,# Generated by, or# DO NOT EDITin 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
namefield orworkspacesentries inpackage.jsonare 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 packagesbower_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,.jsxextensions./lib— triesindex.jsin 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
namefield orworkspacesentries inpackage.jsonare 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 packagesbower_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.tsfiles innode_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,.jsxextensions./lib— triesindex.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 fromsrc/main/java/directory structure - Imports matching the project base package are first-party
- Imports resolvable to files under
src/main/java/orsrc/test/java/are first-party
Third-party/build output paths: Default patterns for Java:
target/— Maven build outputbuild/— 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
@Generatedannotation in the first 20 lines
Module resolution: DiffGuard resolves Java imports using standard Maven/Gradle conventions:
com.example.MyClass→src/main/java/com/example/MyClass.java- Also tries
src/test/java/andsrc/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_relativeimports are always first-party- Standard library modules (
json,net/http,fileutils, etc.) are excluded - Gems listed in
Gemfileare treated as third-party requirecalls that resolve to a local file (underlib/,app/, or project root) are first-party
Third-party/vendor paths: Default patterns for Ruby:
vendor/bundle/— Bundler gem install locationvendor/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 EDITin the first 5 lines
Module resolution: DiffGuard resolves Ruby imports using standard conventions:
require_relative './helper'— relative to current file, tries.rbextensionrequire 'my_app/helper'— trieslib/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:
User→app/models/user.rbUsersController→app/controllers/users_controller.rbAdmin::DashboardController→app/controllers/admin/dashboard_controller.rbUserMailer→app/mailers/user_mailer.rbUserJob→app/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 standardgo generateheader, 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/utils→internal/utils/*.go(first non-test.gofile 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:
usestatements matching PSR-4 namespace prefixes fromcomposer.jsonare first-partyrequire/includewith relative paths (./,../,__DIR__) are first-party- All other
usestatements (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\UserServicewith{"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\User→app/Models/User.phpApp\Http\Controllers\UserController→app/Http/Controllers/UserController.phpApp\Services\Payment\StripeService→app/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 corewp-admin/— WordPress admin corewp-content/cache/— cache plugin output (WP Super Cache, W3 Total Cache)wp-content/uploads/— media uploads directorywp-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 syntaxalias MyApp.{User, Role})import Ecto.Query— function import (with optionalonly:filter)require Logger— compile-time requireuse 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.User → lib/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_app→MyApp) are first-party - Modules matching dependency names from the
depsfunction 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 EDITin 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.PageController→lib/my_app_web/controllers/page_controller.exMyAppWeb.UserLive.Index→lib/my_app_web/live/user_live/index.exMyAppWeb.CoreComponents→lib/my_app_web/components/core_components.exMyAppWeb.Router→lib/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/1helper — bypasses HTML escapingPhoenix.HTML.raw/1in 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 onlangattribute), 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 specifieslang="ts"orlang="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
:iswith 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 onlangattribute), 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 specifieslang="ts"orlang="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:varson<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 withhtml_safe - HEEx:
raw/1helper,Phoenix.HTML.raw/1(bypass HTML escaping) - Blade:
{!! $variable !!}(unescaped),@phpblocks
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 @importSSRF (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_callsto lower the request rate. - Large diffs: Use
max_tokens_per_scanto 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.2instead ofmodel = "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_scanto cap total usage per scan - Reduce
hunk_expansion_linesto send less context per file (default: 50) - Reduce
scope_size_limitto 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.