Scans a branch before merge and tells you exactly how badly it's going to hurt.
Project description
PayloadGuard
Scans a branch before merge and tells you exactly how badly it's going to hurt.
dev: Dark^Vader
Contents
Install
pip install payloadguard-plg
Or from source:
pip install -r requirements.txt
Python 3.8+. Core deps: GitPython, PyYAML, PyJWT, requests. Layer 4 multi-language analysis requires tree-sitter grammar packages (included in requirements.txt โ omit any you don't need, unsupported file types are skipped silently).
Run
python analyze.py <repo_path> <branch> [target]
# Basic scan
python analyze.py . feature-branch main
# Feed it the PR description โ catches deceptive payloads
python analyze.py . feature-branch main --pr-description "minor syntax fix"
# Save a JSON report
python analyze.py . feature-branch main --save-json
# Save a markdown report
python analyze.py . feature-branch main --save-markdown reports/scan.md
python analyze.py --help if you need it.
The forensic report
Every scan produces the same structured report. Here's what each section tells you and what to look for.
๐ Temporal
How old the branch is relative to the target, and which commits are being compared. A branch that's been sitting open for months while the target keeps moving is already a problem before you look at a single line.
๐
TEMPORAL
Branch age: 14 days
Branch: a1b2c3d (2026-04-08)
Target: e4f5g6h (2026-04-22)
๐ File changes
Raw scope of the changeset โ files added, deleted, modified. Deletions are the number to watch. A PR that adds 2 files and deletes 40 is not a normal PR.
๐ FILE CHANGES
Added: 3
Deleted: 1
Modified: 5
Total: 9
๐ Line changes
Volume and direction of change. Deletion ratio is the derived signal โ what fraction of total churn is removal. Above 50% starts raising flags; above 90% means almost everything this PR touches is being taken away.
๐ LINE CHANGES
Added: 420 lines
Deleted: 18 lines
Net: 402 lines
Deletion ratio: 4.1%
๐งฌ Structural drift โ Layer 4
Parses every modified source file and computes exactly which named classes and functions disappeared. This is the layer that catches a file being "modified" when it's actually been gutted โ line diffs alone won't tell you that AuthManager and SessionStore no longer exist.
Flags CRITICAL only when both conditions are met: deletion ratio exceeds the threshold and enough nodes were deleted. The dual gate prevents noise from small utility files.
Supported languages: Python ยท JavaScript ยท TypeScript ยท Go ยท Rust ยท Java (.py .js .jsx .ts .tsx .go .rs .java). Python uses stdlib AST. All others use tree-sitter โ files for grammars not installed are skipped silently.
๐งฌ STRUCTURAL DRIFT (Layer 4)
Overall severity: LOW
Max deletion ratio: 0.0%
If something is actually being removed at scale:
๐งฌ STRUCTURAL DRIFT (Layer 4)
Overall severity: CRITICAL
src/core/auth.py: 8 nodes deleted (80.0%) [CRITICAL]
- AuthManager
- SessionStore
- TokenValidator
โฑ Temporal drift โ Layer 5a
Compound score: branch_age_days ร target_commits_per_day. Raw age alone is a weak signal โ a 90-day branch on a slow repo is nothing; on a fast repo it's a serious semantic gap. The drift score accounts for both.
| Status | Drift Score | Meaning |
|---|---|---|
CURRENT |
< 250 | Branch context is valid |
STALE |
250 โ 999 | Moderate drift โ manual diff review |
DANGEROUS |
โฅ 1000 | Rebase required before this goes anywhere near main |
โฑ TEMPORAL DRIFT (Layer 5a)
Status: CURRENT [LOW]
Drift Score: 14.0
Target velocity: 1.0 commits/day
โ SAFE. Branch context is synchronized with target.
๐ Semantic transparency โ Layer 5b
Compares the PR description against the verified severity. If the description uses low-impact language ("minor fix", "typo", "cleanup") but the structural verdict is CRITICAL, that's a DECEPTIVE_PAYLOAD. Advisory signal โ doesn't override the main verdict, but it shows up clearly.
| Status | Meaning |
|---|---|
TRANSPARENT |
Description matches what the diff actually does |
UNVERIFIED |
No description provided |
DECEPTIVE_PAYLOAD |
Description claims low impact, diff says otherwise |
๐ SEMANTIC TRANSPARENCY (Layer 5b)
Status: TRANSPARENT
โ SAFE. PR description aligns with verified structural impact.
๐ Verdict
The final call. Produced by the consequence model (Layer 3) which accumulates a weighted score across all signals. No single threshold triggers it โ it's the combination that matters.
| Verdict | Severity | Score | Meaning |
|---|---|---|---|
SAFE |
LOW | 0 | Nothing notable. Proceed. |
REVIEW |
MEDIUM | 1โ2 | Minor flags. Worth a look but not alarming. |
CAUTION |
HIGH | 3โ4 | Real signals. Needs proper review before merge. |
DESTRUCTIVE |
CRITICAL | โฅ 5 | Stop. Do not merge. |
๐ VERDICT: SAFE [LOW]
โ ๏ธ No major red flags detected
โ๏ธ RECOMMENDATION:
โ Proceed with normal review process
Exit codes
| Code | Meaning |
|---|---|
0 |
Fine |
1 |
Analysis broke |
2 |
Do not merge โ wire this to block CI |
CI
GitHub Action (recommended)
Add to .github/workflows/payloadguard.yml:
name: PayloadGuard
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: PayloadGuard
id: payloadguard
uses: DarkVader-PLG/payload-consequence-analyser@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-description: ${{ github.event.pull_request.body }}
- name: Enforce verdict
if: always()
env:
EXIT_CODE: ${{ steps.payloadguard.outputs.exit-code }}
run: |
if [ "$EXIT_CODE" = "1" ]; then exit 1; fi
if [ "$EXIT_CODE" = "2" ]; then exit 2; fi
With GitHub App secrets wired up, pass them too:
- name: PayloadGuard
uses: DarkVader-PLG/payload-consequence-analyser@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-description: ${{ github.event.pull_request.body }}
app-id: ${{ secrets.PAYLOADGUARD_APP_ID }}
private-key: ${{ secrets.PAYLOADGUARD_PRIVATE_KEY }}
installation-id: ${{ secrets.PAYLOADGUARD_INSTALLATION_ID }}
Wire a branch protection rule to require the scan check and the merge button is blocked on DESTRUCTIVE verdicts.
GitHub App
For a named PayloadGuard check badge in the PR checks tab (beyond the sticky comment), register a GitHub App and wire it up with three repo secrets:
| Secret | Value |
|---|---|
PAYLOADGUARD_APP_ID |
Your App ID |
PAYLOADGUARD_PRIVATE_KEY |
Contents of the generated .pem private key |
PAYLOADGUARD_INSTALLATION_ID |
Installation ID from github.com/settings/installations |
With those set, the workflow calls post_check_run.py to post a Check Run after each scan โ green for SAFE, red for DESTRUCTIVE, with the full report as the body.
Without the secrets the step is a no-op; the sticky comment and merge blocking still work.
Configuration
Drop a payloadguard.yml in your repo root. Everything is optional โ omit what you don't care about and the defaults hold.
# payloadguard.yml
thresholds:
branch_age_days: [90, 180, 365] # score goes up at each
files_deleted: [10, 20, 50]
lines_deleted: [5000, 10000, 50000]
temporal:
stale: 250 # drift score = age ร commits/day
dangerous: 1000
structural:
deletion_ratio: 0.20 # fraction of AST nodes deleted
min_deleted_nodes: 3 # both must be hit to flag CRITICAL
semantic:
benign_keywords:
- minor fix
- minor syntax fix
- typo
- formatting
- cleanup
- small tweak
Tighten it for anything that matters:
thresholds:
structural:
deletion_ratio: 0.10
min_deleted_nodes: 2
semantic:
benign_keywords:
- minor fix
- typo
- trivial
- nit
How it works
Five layers. Every scan, every time.
| Layer | What it checks |
|---|---|
| 1 โ Surface Scan | Files and lines changed |
| 2 โ Forensic Analysis | Deletion ratio, critical path detection |
| 3 โ Consequence Model | Weighted score โ final verdict |
| 4 โ Structural Drift | AST/tree-sitter diff โ which classes and functions actually disappeared (Python, JS, TS, Go, Rust, Java) |
| 5a โ Temporal Drift | Branch age ร repo velocity |
| 5b โ Semantic Transparency | Does the PR description match what the diff actually does |
Scoring (Layer 3)
Points accumulate across signals. No single threshold kills you โ it's the pile-up that matters.
| Signal | Thresholds | Points |
|---|---|---|
| Branch age | > 90 / 180 / 365 days | 1 / 2 / 3 |
| Files deleted | > 10 / 20 / 50 | 1 / 2 / 3 |
| Deletion ratio | > 50% / 70% / 90% | 1 / 2 / 3 |
| Structural severity | CRITICAL | 3 |
| Lines deleted | > 5k / 10k / 50k | 1 / 2 / 3 |
โฅ 5 โ DESTRUCTIVE. 3โ4 โ CAUTION. 1โ2 โ REVIEW. 0 โ SAFE.
The incident
In April 2026, a developer received a Codex suggestion described as a "minor syntax fix". The branch had been open for 10 months. Nobody looked closely enough. It would have deleted 60 files, 11,967 lines, 217 tests, and the entire application architecture in a single merge. That's what this tool was built to stop.
Below is the forensic report PayloadGuard would have produced on that branch.
======================================================================
PAYLOADGUARD ANALYSIS: codex-suggestion โ main
======================================================================
๐
TEMPORAL
Branch age: 312 days
Branch: fa3c21d (2025-06-04)
Target: b87e90a (2026-04-22)
๐ FILE CHANGES
Added: 2
Deleted: 61
Modified: 4
Total: 67
๐ LINE CHANGES
Added: 214 lines
Deleted: 11,967 lines
Net: -11,753 lines
Deletion ratio: 98.2%
๐งฌ STRUCTURAL DRIFT (Layer 4)
Overall severity: CRITICAL
Max deletion ratio: 94.0%
src/core/auth.py: 12 nodes deleted (94.0%) [CRITICAL]
- AuthManager
- SessionStore
- TokenValidator
- PermissionGate
- RoleRegistry
โฑ TEMPORAL DRIFT (Layer 5a)
Status: DANGEROUS [CRITICAL]
Drift Score: 3120.0
Target velocity: 10.0 commits/day
โ DO NOT MERGE. Extreme semantic drift detected. Mandatory rebase
and manual architectural review required.
๐ SEMANTIC TRANSPARENCY (Layer 5b)
Status: DECEPTIVE_PAYLOAD
Matched keyword: "minor syntax fix"
โ DO NOT MERGE. PR description deliberately contradicts catastrophic
architectural changes.
๐ VERDICT: DESTRUCTIVE [CRITICAL]
โ ๏ธ Branch is 312 days old (6+ months)
โ ๏ธ 61 files would be deleted (massive scope)
โ ๏ธ Deletion ratio: 98.2% (almost entire changeset is deletions)
โ ๏ธ Structural drift CRITICAL โ significant class/function deletions detected
โ ๏ธ 11,967 lines would be deleted (large codebase change)
โ๏ธ RECOMMENDATION:
โ DO NOT MERGE โ This would catastrophically alter the codebase
๐๏ธ DELETED FILES (61 total)
CRITICAL DELETIONS:
- tests/test_auth.py
- tests/test_core.py
- tests/test_integration.py
- .github/workflows/ci.yml
- src/core/auth.py
- src/core/engine.py
- requirements.txt
OTHER DELETIONS:
- src/modules/session.py
- src/modules/permissions.py
- src/modules/roles.py
... and 51 more files
======================================================================
Every signal was there. The age. The deletion ratio. The structural wipeout. The gap between what the description said and what the diff actually did. Nobody saw it. Now you will.
PayloadGuard โ because AI doesn't feel bad about what it breaks.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file payloadguard_plg-1.0.2.tar.gz.
File metadata
- Download URL: payloadguard_plg-1.0.2.tar.gz
- Upload date:
- Size: 23.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
54a6cbd4d7036be3e78623266ca80458ec5d4f889d7ca328ac6c8b9b784b9e79
|
|
| MD5 |
badd32107f91385bad7f114ba635753a
|
|
| BLAKE2b-256 |
dd76a40d3f112f39b0ec1b18ee003d8d86ad3d14a5b307c4b040823e208e2828
|
File details
Details for the file payloadguard_plg-1.0.2-py3-none-any.whl.
File metadata
- Download URL: payloadguard_plg-1.0.2-py3-none-any.whl
- Upload date:
- Size: 20.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c00d3673b8f3859826aaecb267f95aab8efe1e9a341a63cef6bd79a985412436
|
|
| MD5 |
c161cb33a1d79040a8669889bc39ab1f
|
|
| BLAKE2b-256 |
150fb7f6c96b9fb6cd8b99176a5ecd33065942a39bc0c54e6648fae50a502569
|