AI-powered security review for git workflows
Project description
PwnGuard
Status: Proof of Concept (
v0.2.7). PwnGuard is an open-source security tool by Shiva Kerdel (Power to Logic). It flags risky code when committing your work, so issues surface during development instead of in production. Proof of concept: flags and config may change before1.0. It points you in the right direction, but it's not a substitute for proper security review.
AI-powered security review for your git workflow. Catches insecure code before it gets your organization pwned. Runs as a pre-commit hook, in CI/CD, or against open merge requests / pull requests.
Scans git diffs (not full repos) for security issues.
Quick start
pipx install pwnguard # or: uv tool install pwnguard
cd your-project
pwnguard --install-hook # scans staged changes on every git commit
Needs Python 3.9+ and one backend — the claude CLI, a local Ollama
server, or an API key. See Setup for the full picture.
To remove it again:
pwnguard --uninstall-hook # run inside any repo where you installed the hook
pipx uninstall pwnguard # removes the CLI itself
Table of contents
- Quick start
- What it does
- Current focus
- Backends
- Setup
- Configuration files
- Common workflows
- CLI reference
- Configuration reference (
pwnguard.yaml) - Environment variables
- Exit codes
- Output modes
- Remote fetching from GitLab / GitHub
- GitLab CI
- Enforcement
- Limitations
- Security
- Further documentation
- Acknowledgments
What it does
PwnGuard sits between you and a git commit (or between a contributor
and a merge-able MR) and asks a language model to look for security
issues in the diff of what's changing. It returns structured findings
with severity, location, description, recommendation, and (when the
backend can produce them) the offending lines and a suggested fix snippet.
You can also point it at:
- A local file (manual scan)
- A pre-saved diff on disk (offline testing)
- A live GitLab MR / commit or GitHub PR / commit (via API)
For the architecture pipeline and how findings are anchored back to file/line, see docs/architecture.md.
Current focus
The system prompt is tuned for PHP, Shopware 6, and CakePHP 3/4/5, with generic coverage for common web-app vulnerability classes across other languages. The framework hints help the model spot ecosystem-specific patterns; the generic coverage list keeps it useful elsewhere. Other languages / frameworks will land as configurable "profiles" in a future release.
Backends
| Backend | Auth | Cost | Best for |
|---|---|---|---|
claude-code |
Claude Pro subscription | Included in Pro | Local default; broad context, high recall |
ollama |
None | Free | CI/CD, offline, small machines |
claude-api |
ANTHROPIC_API_KEY |
Pay per token | Orgs with API access; CI without runner |
openai-compat |
OPENAI_API_KEY |
Depends on provider | Any OpenAI-compatible endpoint: LiteLLM, vLLM, OpenRouter, Groq, Together, Fireworks, llama.cpp server, LM Studio, etc. |
Auto-detection: locally the tool checks for the claude CLI first and
falls back to Ollama. In CI it uses Ollama by default; if
ANTHROPIC_API_KEY is set, it uses claude-api. The openai-compat
backend is always opt-in via --backend openai-compat.
For picking an Ollama model by VRAM tier, see docs/ollama-guide.md.
Setup
1. Install PwnGuard
Recommended (isolated CLI on PATH, no PEP 668 friction):
pipx install pwnguard
# or:
uv tool install pwnguard
Or into a project venv:
python3 -m venv .venv
source .venv/bin/activate
pip install pwnguard
For the Anthropic API backend, pull in the optional dependency:
pipx install "pwnguard[claude-api]"
# or, on an existing pipx install:
pipx inject pwnguard anthropic
2. Install the pre-commit hook
From the root of the git repository you want to protect:
pwnguard --install-hook
Every git commit now scans the staged diff. The hook is per-clone, so
each contributor runs this once after cloning the project.
Optional: composer auto-install (PHP projects)
To install the hook automatically when each developer runs
composer install, add to your composer.json:
{
"scripts": {
"setup-pwnguard": "pwnguard --install-hook || echo '[pwnguard] not installed; run: pipx install pwnguard'",
"post-install-cmd": ["@setup-pwnguard"],
"post-update-cmd": ["@setup-pwnguard"]
}
}
If you already have a post-install-cmd, add "@setup-pwnguard" to the
existing array.
Requirements
Python 3.9+ and one of the backends below — pick the row that matches your situation. Anything not in the "Required setup" column is optional; PwnGuard ships with sensible defaults for every other knob.
| Your situation | Required setup |
|---|---|
Default: claude CLI is logged in or Ollama is running locally |
Nothing else — pwnguard auto-detects and uses built-in defaults |
| Anthropic API backend | ANTHROPIC_API_KEY env var (or a line in .pwnguard.env); pipx install "pwnguard[claude-api]" to pull in the anthropic package |
| Any OpenAI-compatible endpoint (LiteLLM, vLLM, OpenRouter, Groq, …) | OPENAI_API_KEY env var and an openai: block in pwnguard.yaml setting url and model |
Scanning a GitLab MR / GitHub PR via --from-url |
GITLAB_TOKEN (required for GitLab) and/or GITHUB_TOKEN (required for private GitHub repos) |
Monitor mode (--monitor) |
A monitor.repos: block in pwnguard.yaml — see docs/monitor-mode.md |
| Custom severity threshold, ignore patterns, model, etc. | A pwnguard.yaml containing only the keys you want to override — deep-merged on top of defaults |
For developing PwnGuard itself or running the test suite, see docs/development.md.
Configuration files
PwnGuard auto-loads config from the current working directory at startup —
the same directory git commit runs in, so the pre-commit hook picks up
project-local files automatically. None of these files are required;
without any of them, built-in defaults kick in and a one-line notice
prints to stderr.
| File | Purpose | Committed? |
|---|---|---|
pwnguard.yaml (or .pwnguard.yaml) |
Project config shared with the team | Yes |
pwnguard.local.yaml (or .pwnguard.local.yaml) |
Personal overrides, deep-merged on top of pwnguard.yaml |
No (already in .gitignore) |
.pwnguard.env |
API tokens as KEY=value lines |
No (already in .gitignore) |
.env |
Generic env file, lower precedence than .pwnguard.env |
No |
~/.config/pwnguard/config.yaml |
Global yaml fallback across every project | n/a (home dir) |
~/.config/pwnguard/.pwnguard.env |
Global token fallback across every project | n/a (home dir) |
Precedence (highest wins): --config <PATH> / --env-file <PATH> CLI flags
→ process env vars → project file → home-dir file → built-in defaults.
pwnguard.local.yaml is always deep-merged on top of whichever yaml loaded.
Starter templates for pwnguard.yaml and .pwnguard.env.example live in
the GitHub repo; copy the keys you
need rather than the whole file.
Common workflows
All examples below use the installed
pwnguardCLI (frompipx install pwnguard). The pre-commit hook installed bypwnguard --install-hookruns the same command under the hood.
Local pre-commit (default)
Hook fires automatically on git commit. Scans only the staged diff.
git add src/Controller/UserController.php
git commit -m "feat: add user lookup"
# PwnGuard runs, blocks if HIGH/CRITICAL findings
Manual scan of one or more files
pwnguard --mode manual --files src/Foo.php src/Bar.php
Scan a GitLab merge request
export GITLAB_TOKEN=glpat-...
pwnguard --from-url "https://gitlab.com/grp/proj/-/merge_requests/123"
Scan only one commit of a large MR
pwnguard --from-url \
"https://gitlab.com/grp/proj/-/merge_requests/123/diffs?commit_id=abc1234def"
Scan a GitHub pull request
export GITHUB_TOKEN=ghp_... # optional for public repos, required for private
pwnguard --from-url "https://github.com/owner/repo/pull/42"
Offline review of a saved diff
git diff origin/main...HEAD > /tmp/branch.diff
pwnguard --diff-file /tmp/branch.diff --review
Interactive walk through findings
pwnguard --from-url "<URL>" --review
# Use arrow keys to navigate, right/left to expand/collapse,
# space to mark, q to quit. Full key bindings: docs/review-tui.md
Watch what the model is doing in real time
pwnguard --backend ollama --files src/Foo.php --debug
# Or against any OpenAI-compatible endpoint (LiteLLM, vLLM, etc.):
pwnguard --backend openai-compat --files src/Foo.php --debug
# Shows a "Waiting for response / Model is thinking" spinner during
# prompt processing, then streams tokens to stderr as they arrive,
# then prints per-request stats (tokens, t/s, stop reason).
Save findings to a Markdown report
pwnguard --from-url "<URL>" --report /tmp/findings.md
Bypass the hook for one commit
PWNGUARD_SKIP=1 git commit -m "WIP (intentionally skipping PwnGuard)"
# OR skip every hook for this commit:
git commit --no-verify
Suppress a false positive (pwnguard:ignore)
When a finding is a confirmed false positive, drop it without disabling
the gate for everything else: add a pwnguard:ignore comment in the code
under review. The marker travels in the same diff as the code it excuses,
so the reason is visible right next to it and there's no central file to
maintain. Suppression is scoped to the file the marker is in.
// pwnguard:ignore CWE-89 - parameterized by the ORM layer
$rows = $db->raw("SELECT * FROM users WHERE id = $id");
Marker forms (use any comment syntax your language allows):
| Form | Scope | Drops |
|---|---|---|
pwnguard:ignore CWE-89 |
whole file | findings whose CWE matches |
pwnguard:ignore "sql injection" |
whole file | findings whose title/description contains the quoted text (case-insensitive) |
pwnguard:ignore |
nearby lines | any finding anchored within ~3 lines of the marker |
Trailing text after the form is a free-text reason and is ignored by the
matcher. The scan reports how many findings were suppressed
(N finding(s) suppressed inline), and --json includes a suppressed
count, so suppression is never silent. List every marker in the repo with
git grep pwnguard:ignore.
The marker is matched wherever its text appears in the scanned diff, so avoid writing the literal token in prose or docstrings near code you still want reviewed (it would act as a bare, line-scoped suppression there).
A ready-made fixture lives at
demo/suppression_demo.py:
intentionally vulnerable, with three ignore markers (one per form) plus
unmarked findings that still block, for testing the gate.
CLI reference
Every flag accepted by pwnguard. Default values come from
DEFAULT_CONFIG unless overridden in pwnguard.yaml.
Run mode + diff source
| Flag | Default | Purpose |
|---|---|---|
--mode {hook,ci,manual} |
hook |
Source of the diff. hook = staged diff, ci = MR target-branch diff, manual = files you pass with --files. |
--files <PATH ...> |
(none) | Files to scan in manual mode. Multiple paths allowed. |
--mr-diff |
off | In hook mode, fetch the MR target-branch diff (same as --mode ci). |
--diff-file <PATH> |
(none) | Read a unified diff from disk instead of running git. Useful for offline testing or replaying a saved diff. Input is validated up front: a file that doesn't look like a unified diff (no diff --git / +++ b/ headers) is refused with a precise hint pointing to --mode manual --files <PATH> instead. |
--from-url <URL> |
(none) | Fetch the diff from a GitLab MR / GitHub PR / commit URL via API. See Remote fetching. Same shape-check as --diff-file: if the platform returns HTML (auth failure, rate limit, outage) instead of a diff, the run aborts with a preview of what came back rather than silently scanning nothing. |
Backend + model
| Flag | Default | Purpose |
|---|---|---|
--backend {claude-code,ollama,claude-api,openai-compat} |
auto-detected | Which AI backend to use. Auto-detection picks claude-code if the claude CLI is present, otherwise ollama. openai-compat is opt-in. |
--model <NAME> |
from config | Override the model for the active backend (e.g. qwen2.5-coder:14b, claude-opus-4-7, claude-sonnet-4-6). |
--config <PATH> |
pwnguard.yaml in cwd |
Use a different config file. |
Output + presentation
| Flag | Default | Purpose |
|---|---|---|
--json |
off | Emit findings as JSON on stdout. Mutually useful with --dry-run / piping. |
--quiet |
off | One-line-per-finding output. Good for terse CI logs. |
--no-color |
off | Disable ANSI color and OSC 8 hyperlinks. Also auto-disabled when stdout is not a TTY or when NO_COLOR is set. |
--color |
off | Force color and hyperlinks on even when stdout is not a TTY - needed when a git hook manager or composer run-script captures output through a pipe. Mirrors the FORCE_COLOR env var; --no-color / NO_COLOR win if both are set. In this case the terminal width is read from /dev/tty so boxes still fill the screen. |
--code-preview {auto,on,off} |
auto |
Show the affected-code block + Example: fix snippet. auto = on for claude backends, off for ollama (smaller models give imprecise line numbers and skip fix examples). |
--platform {auto,plain,gitlab,github} |
auto |
CI log rendering. gitlab / github fold each finding into a collapsible, collapsed-by-default log section. auto detects GITLAB_CI / GITHUB_ACTIONS from the environment (and inside those, also forces color on and widens output to 120 columns; set COLUMNS to change it), else plain. See docs/ci-cd.md. |
--report <PATH> |
(none) | Write the findings as a Markdown report to <PATH>. |
--debug |
off | Stream the model's output live to stderr (ollama + openai-compat backends). During prompt processing a "Waiting for response..." / "Model is thinking..." spinner shows that the server is active; once tokens start arriving the spinner exits and the stream takes over. Prints per-request stats at the end (token count, tokens-per-second, stop reason). Useful when scans return empty or stop unexpectedly. |
--show-observations |
off | Also surface a short list (max 5) of neutral observations about defensive patterns the model noticed in the diff (e.g. "parameterised query used", "output escaped"). Opt-in and additive: never replaces findings, never claims code is secure. Rendered dim and clearly labelled "informational only" so it can't compete with HIGH/CRITICAL signal. Adds a small number of prompt + output tokens. |
Decision flow
| Flag | Default | Purpose |
|---|---|---|
--threshold {CRITICAL,HIGH,MEDIUM,LOW,INFO} |
from config (HIGH) |
Severity threshold that blocks (exit 1). |
--report-only |
off | Advisory mode: still report findings (log + MR comment) but exit 0 even above the threshold, so a CI job passes and never blocks the merge. A genuine run error still exits 2. Drop it to enforce. |
--dry-run |
off | Show what would be sent to the AI (files, diff size, token estimate) without making the API call. |
--review |
off | After the scan, drop into an interactive TUI to step through findings. See docs/review-tui.md. When a hook scan blocks a commit in an interactive terminal, PwnGuard prints a tip to re-run with --review --cached. |
--cached |
off | Reuse the most recent scan of the identical staged diff instead of re-running the AI - pairs with the --review re-run after a hook scan. Content-keyed on diff + backend + model + pwnguard version, stored in .git/pwnguard-scan-cache.json; any change misses and re-scans. Falls back to a fresh scan on any miss, errored scans are never cached, and the security gate (a bare hook run) never reads it. Staged-diff path only. |
--explain <N> |
(none) | Re-query the AI for a deeper explanation of finding number N (1-indexed). Adds one extra AI call. |
Performance / handling for large diffs
| Flag | Default | Purpose |
|---|---|---|
--chunk-per-file |
off (auto-enabled on overflow) | Split the diff at diff --git boundaries and scan each file separately, then merge findings. Auto-enabled when the estimated prompt+response exceeds Ollama's num_ctx. See docs/ollama-guide.md. |
--format {json,raw} |
json for ollama, raw for openai-compat |
Model output mode (alias: --ollama-format). json constrains output to valid JSON (Ollama constrained generation; openai-compat response_format=json_object): reliable but slower, and not supported by every openai-compat server. raw lets the model emit freely and relies on PwnGuard's parse fallbacks. |
Environment / credentials
| Flag | Default | Purpose |
|---|---|---|
--env-file <PATH> |
(none) | Load KEY=VALUE pairs from <PATH> into the environment before running. .env and .pwnguard.env in the current directory are also auto-loaded; existing process env vars always take precedence. |
Meta
| Flag | Default | Purpose |
|---|---|---|
--version |
- | Print version and exit. |
-h, --help |
- | Show help and exit. |
Configuration reference (pwnguard.yaml)
Every config key, its default, and what it controls. PwnGuard looks for
pwnguard.yaml in the current directory, then .pwnguard.yaml, then
~/.config/pwnguard/config.yaml. Override with --config <PATH>.
Top-level
| Key | Default | Purpose |
|---|---|---|
severity_threshold |
HIGH |
Minimum severity that blocks commits / merges. One of CRITICAL / HIGH / MEDIUM / LOW / INFO. |
backend |
(auto-detect) | Pin the AI backend for this project (overrides hook/ci auto-detection). One of claude-code, ollama, claude-api, openai-compat. The --backend CLI flag always wins; per-developer override goes in pwnguard.local.yaml. |
ignore_patterns |
see source | Glob patterns to skip (per file). Defaults skip vendor/, node_modules/, *.min.js/css/map, lock files, common test paths. |
language_focus |
[php, js, ts, twig, python] |
File extensions to scan (others are filtered out). Empty list = scan all. |
max_diff_lines |
500 |
Cap on unified-diff lines sent to the AI (non-chunked mode only). Lines beyond the cap are dropped with a [TRUNCATED] marker. Chunked mode skips this cap because per-file splitting handles size. |
max_file_size_kb |
100 |
Cap on a single file's size (KB) in --mode manual. Files larger than this are skipped with a [SKIPPED] marker. |
claude_code: (uses the claude CLI)
| Key | Default | Purpose |
|---|---|---|
timeout |
120 |
Wall-clock seconds for one claude invocation. |
claude_api: (uses the Anthropic API)
| Key | Default | Purpose |
|---|---|---|
model |
claude-opus-4-7 |
Model to call. Pick from Anthropic's available IDs. |
max_tokens |
4096 |
Output budget per response. |
ollama: (uses a local Ollama server)
| Key | Default | Purpose |
|---|---|---|
model |
qwen2.5-coder:7b |
Ollama model tag. |
url |
http://localhost:11434 |
Ollama server URL. |
allow_remote |
false |
Safety: only localhost / 127.0.0.1 / ::1 are allowed unless this is set true. Prevents an attacker-controlled pwnguard.yaml from redirecting diffs to a remote endpoint. |
timeout |
600 |
Wall-clock seconds per Ollama request. Higher than other backends because large diffs on 7B models can take several minutes for prompt processing alone. |
keep_alive |
(model default) | How long Ollama keeps the model resident in VRAM after a request. Set e.g. "30m" to skip the model-reload tax on back-to-back scans. |
num_ctx |
(model default) | Context window size. Bigger fits more diff but uses more VRAM. Note: changing this at request-time forces Ollama to reload the model; for a stable large context build a Modelfile. |
num_predict |
(model default) | Cap on output tokens. Useful for predictable wall time. |
temperature |
(model default) | Lower = more deterministic. PwnGuard's shipped config uses 0.5 for exploration. |
seed |
(random) | Pin the RNG for fully reproducible runs. Comment out for variation. |
openai: (uses any OpenAI-compatible Chat Completions endpoint)
Works against LiteLLM, vLLM, OpenRouter, Groq, Together, Fireworks,
llama.cpp server, LM Studio, Ollama's /v1 mode, etc. Activate with
--backend openai-compat.
| Key | Default | Purpose |
|---|---|---|
url |
https://api.openai.com |
Base URL. /v1/chat/completions is appended automatically. |
model |
gpt-4o-mini |
Model name as the upstream endpoint expects it. |
api_key_env |
OPENAI_API_KEY |
Name of the env var to read the Bearer token from. The key never lives in the yaml. |
allow_insecure |
false |
Allow plaintext HTTP to non-loopback hosts. Default blocks it so the Bearer token + diff can't be intercepted on the wire. Loopback (localhost / 127.0.0.1 / ::1) is always allowed regardless. |
timeout |
600 |
Wall-clock seconds per request. Raise for very large diffs or slow proxies. |
num_predict |
(server default) | Max output tokens (maps to OpenAI's max_tokens). |
temperature |
(server default) | Lower = more deterministic. |
seed |
(server default) | Pin the RNG for reproducible runs (only honored by some providers). |
top_p |
(server default) | Nucleus-sampling cutoff. |
Security guards: the URL scheme is restricted to http / https (no
file:// / ftp://); HTTP redirects are refused so the Bearer token
can never be forwarded across hosts; the destination host is printed
on every run so a stealth yaml edit is visible.
gitlab: (MR comment posting)
How findings are posted back to the merge request on --mode ci --mr-diff
(needs GITLAB_TOKEN or CI_JOB_TOKEN).
| Key | Default | Purpose |
|---|---|---|
comment_as_thread |
true |
true posts a resolvable discussion thread (must be resolved before merge when the project requires resolved threads); false posts a plain, non-resolvable note. A clean pass / error is always a plain note. |
comment_collapsed |
true |
true wraps the finding detail in a folded <details> block, keeping the heading and severity tally visible; false posts the full detail inline. |
Local override (pwnguard.local.yaml)
PwnGuard also auto-loads a gitignored pwnguard.local.yaml (or
.pwnguard.local.yaml) and deep-merges it on top of the main config.
Use it for machine-specific values that shouldn't land in the
committed config:
# pwnguard.local.yaml (gitignored)
openai:
url: https://litellm.internal.example.com
model: qwen3-coder-480b
Both file names are in the shipped .gitignore. The same pattern
works for any config key, not just openai:.
Environment variables
PwnGuard auto-loads .pwnguard.env and .env from the current directory
at startup. You can also pass --env-file <PATH> for an explicit file.
Process env always wins over what's loaded from a file.
| Var | Required when | Purpose |
|---|---|---|
ANTHROPIC_API_KEY |
using --backend claude-api |
Anthropic API token. |
OPENAI_API_KEY (or the var named by openai.api_key_env) |
using --backend openai-compat |
Bearer token sent to the OpenAI-compatible endpoint. Required - PwnGuard never reads it from the yaml. |
GITLAB_TOKEN (or PWNGUARD_GITLAB_TOKEN) |
using --from-url against a GitLab URL |
Personal Access Token with api or read_api scope. |
GITHUB_TOKEN (or PWNGUARD_GITHUB_TOKEN) |
private GitHub repos + rate-limit lift | GitHub PAT or fine-grained token. Optional for public repos. |
PWNGUARD_SKIP |
one-off | If set to 1, the pre-commit hook exits 0 immediately. Bypasses just PwnGuard, leaves other hooks running. |
PWNGUARD_NO_PROMPT |
one-off | Suppress the "large prompt" confirmation that claude-api shows when a scan would exceed ~50k tokens. Useful for non-interactive scripts. |
NO_COLOR |
one-off | Standard convention. When set, ANSI styling is disabled (same as --no-color). |
CI_MERGE_REQUEST_TARGET_BRANCH_NAME |
--mode ci |
GitLab CI provides this automatically. PwnGuard validates it and uses git diff origin/<branch>...HEAD. |
CI_PROJECT_ID, CI_MERGE_REQUEST_IID, CI_SERVER_URL, GITLAB_TOKEN / CI_JOB_TOKEN |
posting comments back to a GitLab MR | All needed to post the audit result as an MR comment. |
A starter .pwnguard.env.example is in the repo with every variable
listed and links to where to generate the tokens.
Exit codes
| Exit | Meaning |
|---|---|
0 |
No findings at or above threshold; commit/merge may proceed. |
1 |
Findings exceed threshold; commit blocked. |
2 |
Audit could not complete (config error, AI backend failure, malformed response). |
Wire CI accordingly: treat 2 as "investigate", not as "approved".
Output modes
The same scan can produce different output shapes based on the flags.
Default (text, grouped by file)
Bold severity badge on the left, title, dim path:line / CWE on the right.
Body shows the affected code lines, the description, and a bold-green
Fix: recommendation inside a dashed card.
--quiet
Same title row layout, but no body block. One line per finding. Good for CI logs where the description doesn't add value.
--json
Machine-readable. Useful for piping into other tools:
{
"findings": [
{
"severity": "HIGH",
"confidence": "high",
"title": "sql injection via user_id in lookup_user",
"file": "app/users.py",
"line": 244,
"anchor": "a10",
"description": "...",
"recommendation": "...",
"cwe": "CWE-89",
"fix_example": "cur.execute('SELECT ... WHERE id = ?', (user_id,))",
"hunk_context": "def lookup_user(user_id):"
}
],
"summary": { "HIGH": 1, "MEDIUM": 2 },
"files_scanned": 3,
"threshold": "HIGH",
"blocked": true,
"elapsed_seconds": 4.2
}
file and line are populated by the host after the model echoes back
its anchor token; consumers can rely on them. The anchor itself is
included for debugging / traceability.
--report <PATH>
Writes the same content as the GitLab MR comment to a Markdown file. Useful for archiving scans or attaching to issues.
--show-observations (opt-in)
When set, the model is also asked to list up to 5 neutral observations
about defensive patterns it spotted in the diff (e.g. "parameterised
SELECT", "htmlspecialchars before echo", "CSRF token compared with
session"). The block is rendered dim under the findings (or under the
PASS line when there are none) and labelled "informational only,
not security validation":
Observations · informational only, not security validation
· parameterised SELECT src/User.php:38 user_id bound via PDO
· htmlspecialchars on output src/Show.php:92 HTML-escape before echo
Why opt-in: the model is forbidden from claiming code is "secure" or "safe" - observations only describe patterns observed. Even so, a list of "good things" can subtly give a reviewer a reason to discount real findings, so the default behaviour stays "silent on success" and this feature must be turned on per run. Adds a small number of prompt and output tokens. Works best on Claude backends; 7B Ollama models may produce noisy observations.
--debug
Stream-style. Live model output to stderr while the scan runs.
Supported on the ollama and openai-compat backends. Three phases:
- Waiting for response - spinner with elapsed seconds (request in flight, server hasn't replied yet).
- Model responding / Model is thinking - spinner label flips once any chunk arrives; "thinking" specifically when a reasoning model (DeepSeek R1, Qwen-thinking, etc.) is emitting
reasoning_contentbefore its actual answer. - Live token stream - spinner exits, tokens echo to stderr as they're generated.
Per-request stats print at the end:
PwnGuard: prompt: 6,142 tokens · output: 423 tokens · 84.2 t/s · stop: stop
Remote fetching from GitLab / GitHub
--from-url <URL> fetches the diff via the platform API. Auto-detects
the type from URL shape:
| URL shape | Effect |
|---|---|
https://<gl>/<group>/<proj>/-/merge_requests/<n> |
Full MR diff |
https://<gl>/<group>/<proj>/-/merge_requests/<n>/diffs?commit_id=<sha> |
One commit inside the MR (handy for chunked review of big MRs) |
https://<gl>/<group>/<proj>/-/commit/<sha> |
Standalone commit |
https://github.com/<owner>/<repo>/pull/<n> |
GitHub PR |
https://github.com/<owner>/<repo>/commit/<sha> |
GitHub commit |
Self-hosted GitLab works (any host with the standard /-/ URL shape).
GitHub Enterprise works via /api/v3 on the same host.
Tokens are read from env vars (see above) or from .pwnguard.env.
GitLab CI
Add the security stage from .gitlab-ci.example.yml to your pipeline.
Two options:
Option A: Self-hosted runner with Ollama (no API key needed)
- Install Ollama on the runner
- Pull the model:
ollama pull qwen2.5-coder:7b - Tag the runner as
security-runner
Option B: Claude API (if your org has API access)
- Add
ANTHROPIC_API_KEYto CI/CD Variables (masked, protected)
Both fail the pipeline on HIGH/CRITICAL findings and post results as MR
comments. A failed pipeline only blocks the merge once you enable
Settings > Merge requests > Merge checks > "Pipelines must succeed".
For comment posting, set GITLAB_TOKEN (or use the auto-provided
CI_JOB_TOKEN for project-internal access).
By default PwnGuard posts findings as resolvable discussion threads
(gitlab.comment_as_thread). To make those threads a merge blocker of
their own - each one must be resolved before the MR can merge - also
enable Settings > Merge requests > Merge checks > "All threads must be
resolved". Leave it off (or set comment_as_thread: false) to keep the
comments purely informational.
Inside a pipeline PwnGuard auto-detects GitLab/GitHub and folds each
finding into a collapsible log section, forces color on, and widens the
report to 120 columns. The job image also needs git (the slim Python
images don't ship it). See
docs/ci-cd.md.
For GitHub Actions, a generic example, and how to rehearse the gate locally before configuring any pipeline, see docs/ci-cd.md.
Enforcement
| Layer | Threshold | Bypassable |
|---|---|---|
| Pre-commit (local) | HIGH | Yes (--no-verify or PWNGUARD_SKIP=1) |
| GitLab CI (pipeline) | HIGH | Depends on allow_failure |
Both enforce the same threshold. Local is fast feedback. In CI, the
pwnguard job's allow_failure picks the mode: false (blocking) makes
a finding block the merge once "Pipelines must succeed" is enabled;
true (advisory) still runs and comments but never blocks - good for
trialing PwnGuard on a project. See the GitLab CI section for both.
Limitations
- AI produces false positives. Review findings before acting; suppress a
confirmed one with a
pwnguard:ignorecomment (see Suppress a false positive). - Cannot see runtime behaviour, only static code.
- Large diffs may be truncated on non-chunked runs (
max_diff_lines); chunked mode handles this by splitting per file / per hunk. - Cross-file context is reduced in chunked mode; cross-hunk context is reduced when a single file is sub-split.
- Ollama (especially 7B) is less accurate than Claude for security
review.
--code-preview autohides the affected-code block on the ollama backend so missing-fix-example fields don't leave empty cards. The red-target marker on the exact line is currently suppressed globally (the model can still anchor to a function header instead of the inner statement); the ±3-line context window still renders so you see the area. File and line themselves are resolved via opaque anchor tokens and are reliable - the marker suppression is only about which row inside the window the model picked, not about whether the location is correct. - Reproducibility on Ollama depends on
seed+temperature; without pinning the seed, the same diff can produce different findings across runs. - Does not replace penetration testing or manual code review.
- Currently tuned for PHP / Shopware / CakePHP, with generic coverage for other languages. A profile system for full multi-stack support is on the roadmap.
Security
Please report vulnerabilities privately via the process in SECURITY.md.
Further documentation
In-depth guides live under docs/:
- Architecture & opaque anchor tokens — how the diff is tagged, sent to the model, and resolved back to file/line.
- Interactive review TUI —
--reviewkey bindings and workflow. - Monitor mode (
--monitor) — dashboard for watching remote repos and auditing each new commit as it lands. - CI/CD usage — gating a pipeline by exit code, GitHub Actions / GitLab / generic examples, rehearsing the gate locally, and suppressing false positives in CI.
- Choosing an Ollama model & handling large diffs — local-model picks by VRAM tier, plus the chunked-mode trade-off when a diff overflows the context window.
- Development & testing — running the pytest suite, the
--self-testentry point, and the pre-commit auto-run for this repo.
Acknowledgments
PwnGuard is built and maintained by Power to Logic, originally created at Let's Talk and open-sourced with their kind permission.
Project details
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 pwnguard-0.2.7.tar.gz.
File metadata
- Download URL: pwnguard-0.2.7.tar.gz
- Upload date:
- Size: 184.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
377a30a4415528b06a168d2dafdbccc762aef74a04c8500dc563495db47c0b82
|
|
| MD5 |
6f601aeeb8213f6f4ba04bb989a95a76
|
|
| BLAKE2b-256 |
d5ed5bc2d8cc0523b4d642b4a9a10867cad82b9b130559ea67011cfcb8f45a21
|
Provenance
The following attestation bundles were made for pwnguard-0.2.7.tar.gz:
Publisher:
publish.yml on s-kerdel/PwnGuard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pwnguard-0.2.7.tar.gz -
Subject digest:
377a30a4415528b06a168d2dafdbccc762aef74a04c8500dc563495db47c0b82 - Sigstore transparency entry: 2129097267
- Sigstore integration time:
-
Permalink:
s-kerdel/PwnGuard@abeb39fb2e2ae69807e770df012053ab4177cce5 -
Branch / Tag:
refs/tags/v0.2.7 - Owner: https://github.com/s-kerdel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@abeb39fb2e2ae69807e770df012053ab4177cce5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pwnguard-0.2.7-py3-none-any.whl.
File metadata
- Download URL: pwnguard-0.2.7-py3-none-any.whl
- Upload date:
- Size: 175.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c7f2098f0943ef3a2d7d91413869a5f67b55bb91328f6788eed4515cd4472ed7
|
|
| MD5 |
9b7cf0d8cd81b0f34fa03f880d38b73a
|
|
| BLAKE2b-256 |
72deda7ab307e21127b84ffa17972012feceda0e7e9466d34bb61c0d24891939
|
Provenance
The following attestation bundles were made for pwnguard-0.2.7-py3-none-any.whl:
Publisher:
publish.yml on s-kerdel/PwnGuard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pwnguard-0.2.7-py3-none-any.whl -
Subject digest:
c7f2098f0943ef3a2d7d91413869a5f67b55bb91328f6788eed4515cd4472ed7 - Sigstore transparency entry: 2129097888
- Sigstore integration time:
-
Permalink:
s-kerdel/PwnGuard@abeb39fb2e2ae69807e770df012053ab4177cce5 -
Branch / Tag:
refs/tags/v0.2.7 - Owner: https://github.com/s-kerdel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@abeb39fb2e2ae69807e770df012053ab4177cce5 -
Trigger Event:
push
-
Statement type: