← Getting Started

Configuration Reference

Every knob PullGuard exposes. All fields are optional — the scanner works out of the box with sensible defaults. Override only what you need.

On this page

Workflow inputs

Pass values via the with: block on pullguard-dev/pullguard-action@v1. Every input is optional.

Input Default What it does
license-key none Your pg_live_* token from Stripe checkout. Unlocks Pro (42 of 44 analyzers, 1 repo), Team (all 44, 10 repos), or Enterprise (all 44, repo bands + 4 business-hour SLA + SSO/RBAC; audit-log on the roadmap). Leave empty for the Free tier (14 analyzers).
fail-on-severity empty Fail the workflow if any finding at this severity or above exists. Values: info, minor, moderate, major, critical. Empty (default) means PullGuard only reports — never blocks merge.
hourly-rate 150 Developer hourly rate in USD. Used to convert finding-effort estimates into dollar figures in the cost-of-change report. Set to your fully-loaded engineer rate for an accurate ROI signal.
config auto-detect Path to a .driftrc.yml file relative to the repo root. If not set, PullGuard looks for .driftrc.yml at the repo root.
path . Subdirectory to scan, relative to the repo root. Default scans the whole repo. Useful for monorepos — run one PullGuard step per package, each with its own path + config.
report-to-app false Enterprise When true, post the report to the PullGuard GitHub App so findings render as native Check Run annotations on the PR Files-changed view. Requires the Marketplace App to be installed.
delta false Force “Clean as You Code” delta filtering — surface only findings this change introduced (plus security criticals) — on non-PR events (push, schedule, workflow_dispatch). On pull_request this is already automatic.
base-ref PR target Git ref to diff against when delta filtering off-PR (e.g. a release tag or origin/main). Ignored on pull_request (the PR target is used). Requires a deep checkout (fetch-depth: 0) so the ref is present.
image-pin 1 Which scanner image the Action runs. 1 (default) = the stable major line — always the newest signed v1.x release, re-pointed only when a release is cut, never per-merge. latest = the bleeding-edge image rebuilt on every merge. Pin an exact release tag (v1.3.4) or a sha256:<hex> digest to freeze one version for change-controlled / air-gapped environments (release tags are preserved forever).
update-baseline false On a push to your base branch, write/refresh .drift-baseline.json from a full scan so future PR deltas match your tier and engine exactly. Your workflow commits the file. Refuses to run on a PR/delta scan.
blame false Annotate each finding with its git-blame introduced date + author in the HTML report and dashboard. Requires a deep checkout — add fetch-depth: 0 to your actions/checkout step; on a shallow checkout PullGuard warns and leaves findings undated (it never fetches history on your behalf). Adds scan latency; off by default.
collapse-preexisting-security false Relocate security findings already in your committed baseline into a counted, collapsed section of the PR comment, so the inline list is just what this PR introduced. New security still shows inline; nothing is hidden from the full report, SARIF, or the fail-on-severity gate. Requires a tier-matched baseline.
server-url none Enterprise Base URL of your self-hosted PullGuard server — when set (with server-token), scan results (never source) upload to your central dashboard after each scan. Best-effort: a failed upload never fails your build.
server-token none Enterprise Ingest bearer token for your self-hosted server (pair with server-url). Store it as a repository secret.

Outputs (for downstream steps)

Output Type What it contains
steps.<id>.outputs.score integer 0–100 Drift score (lower is better). Drives Grade.
steps.<id>.outputs.grade A–F Letter grade derived from score and severity mix.
steps.<id>.outputs.findings integer Total finding count (all severities).

Example: fail builds on Major+, custom rate

      - id: pullguard
        uses: pullguard-dev/pullguard-action@v1
        with:
          license-key: ${{ secrets.PULLGUARD_LICENSE_KEY }}
          fail-on-severity: major
          hourly-rate: 220
          path: services/payments

      - name: Post score to internal dashboard
        if: always()
        run: |
          curl -X POST https://dashboard.example.com/scores \
            -d "score=${{ steps.pullguard.outputs.score }}" \
            -d "grade=${{ steps.pullguard.outputs.grade }}"

.driftrc.yml

Per-repository configuration file at the repo root. All fields are optional. An empty file is valid — the schema fills in defaults so you only specify what you want to override.

Schema validation runs on load. Invalid types or out-of-range values produce a clear error before the scan starts (no silent ignores).

Top-level fields

Field Type Default Purpose
exclude string[] standard ignores Glob patterns appended to the built-in exclude list (node_modules, dist, build, .git, etc.). User patterns add to defaults — you cannot accidentally start scanning node_modules.
maxDepth number 8 Maximum directory depth to traverse. Guards against symlink loops.
maxFiles number auto Hard cap on files analyzed. Hit this in a monorepo → scan per package instead.
analyzers map {} Per-analyzer enable / severity / options overrides. See Analyzers block.
thresholds map see below Numeric thresholds for complexity / duplication / monolithic-file / nesting / type-coverage.
output map see below Format, minimum severity, grouping, remediation toggle.
plugins string[] [] Explicit plugin allowlist. Plugins are sandboxed; auto-discovery is disabled by design.
baseline string none Path to a baseline report — the scanner reports only NEW findings vs the baseline.
cost { enabled, hourlyRate } enabled: true, hourlyRate: 150 hourlyRate — centrally-managed dev rate for the cost estimate (alternative to the workflow input). enabled: falsehide the cost-of-fix breakdown entirely (the “Est. fix cost” headline and the Cost Breakdown section). Findings, severities, and counts still render — useful for security-focused teams that prefer no dollar figures.
compliance map 5-framework summary All 5 frameworks get a compact PASS/CONCERN/FAIL summary on every PR; set hipaa/pci-dss/nist/iso-27001 to true for full per-control detail tables.
repo { type } auto-detect Override the repo-type heuristic that gates OSS-hygiene checks.
taint map {} Custom taint sources, sinks, and sanitizers for proprietary frameworks.
db map OSV remote Local vulnerability database location and freshness policy. See Air-gapped mode.

analyzers: — per-analyzer overrides

Keyed by analyzer ID (e.g. builtin/complexity, builtin/security). Each entry can disable the analyzer, override its default severity, or pass analyzer-specific options.

analyzers:
  builtin/complexity:
    enabled: true
    severity: minor          # downgrade complexity findings
  builtin/duplication:
    enabled: false           # turn this analyzer off entirely
  builtin/security:
    enabled: true            # security analyzers cannot be disabled
                             # (validation rejects this with a clear error)
  builtin/naming:
    severity: info
    options:
      ignorePatterns:
        - "_test\\.ts$"

Note: security-category analyzers cannot be disabled or downgraded below their built-in floor — the schema rejects attempts to do so. This is the integrity guarantee enterprise buyers (CISO / SOC auditor) rely on.

thresholds: — numeric tuning

thresholds:
  complexity:
    maxCyclomatic: 15        # default 10
    maxCognitive: 20         # default 15
    maxParameters: 6         # default 5
    maxFunctionLength: 80    # default 50 (honest line count, not raw)
  duplication:
    minBlockSize: 6          # default 5 (lines)
    maxDuplicationPercent: 5 # default 3 (% of total LoC)
  monolithicFile:
    maxFileLines: 600        # default per-language; this overrides for all
    maxExports: 30           # default 20
  nesting:
    maxDepth: 5              # default 4
  typeCoverage:
    minCoveragePercent: 90   # default 85
    maxAnyPerFile: 3         # default 5

Per-language defaults (Java 1000 / Go 400 / TS 600 etc.) apply automatically. Override with a single thresholds.monolithicFile.maxFileLines value to apply globally; per-language overrides are roadmap.

output: — format and noise control

output:
  format: markdown           # text | json | sarif | markdown
  minSeverity: moderate      # info | minor | moderate | major | critical
  groupBy: severity          # category | severity | file
  showRemediation: true
  sarif:
    toolName: "PullGuard"
    toolVersion: "1.0.0"

Note: html and compliance are CLI --format values, not config values — neither is a valid output.format in .driftrc.yml (the schema rejects them). Use pullguard scan . --format html for the HTML report and the compliance: block below for framework opt-ins.

minSeverity filters the rendered output but does NOT change what is scanned — the JSON artifact always contains every finding for archive / audit purposes.

compliance: — framework opt-ins

compliance:
  hipaa: true
  pci-dss: true
  nist: false
  iso-27001: false

Every PR shows a compact summary of all five frameworks — SOC 2, HIPAA, PCI DSS, NIST 800-53, and ISO 27001 — one PASS / CONCERN / FAIL line each, with no configuration required. Setting a framework to true adds its full per-control evidence table to the report (SOC 2's full grid always renders; the keys are hipaa, pci-dss, nist, iso-27001). Each section emits a "provides evidence for" disclaimer — PullGuard helps auditors; it does not grant compliance. The HIPAA / PCI / NIST / ISO control catalog is fetched at scan time (signed and verified) and degrades to a minimal embedded set, flagged "limited coverage", if your runner can't reach pullguard.dev.

repo.type: — hygiene-check gating

Some checks are appropriate for public OSS but irrelevant for private customer-delivery repos (e.g. missing LICENSE or SECURITY.md). PullGuard auto-detects via GitHub Actions env + local signals; override here when the heuristic is wrong.

repo:
  type: customer-delivery    # public-oss | private-enterprise |
                             # customer-delivery | internal-service |
                             # fork-research

taint: — custom sources, sinks, sanitizers

Enterprise CMS platforms, internal RPC layers, and proprietary frameworks have their own taint surfaces that the built-in patterns do not know about. Add them here. Patterns are language-keyed regular expressions.

taint:
  sources:
    java:
      - "\\bContentManagementData\\.get\\s*\\("
    python:
      - "\\binternal_rpc_call\\s*\\("
  sinks:
    java:
      - "\\bTemplateEngine\\.render\\s*\\("
  sanitizers:
    - "\\bSecurityUtils\\.escape\\s*\\("

cost: — centrally-managed hourly rate

cost:
  hourlyRate: 220

Same effect as the hourly-rate workflow input but lives in the repo so platform teams can manage rate centrally without editing every workflow file.

plugins: — explicit allowlist

plugins:
  - "@acme/pullguard-plugin-payments-rules"
  - "@acme/pullguard-plugin-internal-rpc"

Plugin auto-discovery is intentionally disabled. Each plugin must be explicitly named here AND installed in the runner. Plugins execute in a sandboxed context with a limited API surface — supply-chain defence by design.

Complete .driftrc.yml reference

Every option, with defaults and a one-line comment each. All keys are optional — an empty file is valid; set only what you want to override. Copy this as a starting point. (.driftrc.yaml / .driftrc.json are also accepted.) Baseline / delta options are covered under the schema table above and on the Reports & Dashboard page.

# .driftrc.yml — PullGuard configuration reference (every option, with defaults).
# Place at the root of the repo PullGuard scans. All keys are OPTIONAL — an empty
# file is valid. Set only what you want to override.

# ── File discovery ───────────────────────────────────────────────────────────
exclude:                       # APPENDED to built-ins (node_modules, dist, .git, target, build…)
  - "**/generated/**"
  - "**/*.min.js"
maxDepth: 15                   # default 15 — max directory depth
maxFiles: 5000                 # default 5000 — monorepo safety cap

# ── Per-analyzer overrides (key = analyzer ID; run `pullguard list-rules`) ─────
analyzers:
  builtin/naming-conventions:
    enabled: true              # default true
    severity: minor            # escalate/downgrade (info|minor|moderate|major|critical)
  builtin/duplication:
    enabled: false             # turn an analyzer off entirely
  # Bring-your-own policy rules (Enterprise). pattern = "must NOT match";
  # mustContain = "must match". `pullguard import-semgrep <rules.yml>` emits this.
  builtin/custom-rules:
    options:
      rules:
        - id: no-system-out
          description: "Use the logger, not System.out.println"
          pattern: '\bSystem\.out\.println\s*\('   # violation when present
          severity: major
          files: '**/*.java'

# ── Numeric thresholds ────────────────────────────────────────────────────────
thresholds:
  complexity:
    maxCyclomatic: 15          # default 15
    maxCognitive: 20           # default 20
    maxParameters: 5           # default 5
    maxFunctionLength: 300     # default 300 (comments/blanks stripped first)
  duplication:
    minBlockSize: 6            # default 6 — min lines to count as a clone
    maxDuplicationPercent: 5   # default 5
  monolithicFile:
    maxFileLines: 500          # default 500
    maxExports: 20             # default 20
  nesting:
    maxDepth: 4                # default 4
  typeCoverage:
    minCoveragePercent: 80     # default 80
    maxAnyPerFile: 10          # default 10

# ── Output ────────────────────────────────────────────────────────────────────
output:
  format: markdown             # text (default) | json | sarif | markdown
                               # ('html' and 'compliance' are CLI --format values, not config values)
  minSeverity: moderate        # default info — set 'moderate' to cut CI noise
  groupBy: file                # category (default) | severity | file
  showRemediation: true        # default true
  sarif:
    toolName: PullGuard        # default PullGuard
    toolVersion: "1.3.4"

# ── Cost-of-change estimate ───────────────────────────────────────────────────
cost:
  enabled: true                # default true — false suppresses all $ figures
  hourlyRate: 150              # default 150 (USD)

# ── Over-time dashboard (pullguard dashboard → self-contained HTML) ───────────
dashboard:
  retainScans: 50              # default 50 — per-scan findings kept for drill-down
                               # in .drift-scan-details.json; 0 = keep all scans

# ── SLA / aging gate (flag findings open past a per-severity age budget) ──────
# Age measured from firstSeenAt (.drift-history.json — commit it).
sla:
  critical: 7                  # days — flag a critical open longer than this
  major: 30
  moderate: 90                 # all five severities are supported —
  minor: 180                   # omit a severity to set no SLA for it
  info: 365
  failBuild: false             # default false — true = exit non-zero on any breach

# ── Compliance evidence (SOC 2 always on; these add framework tables) ─────────
compliance:
  hipaa: false                 # default false
  pci-dss: false               # default false  (note the hyphen)
  nist: false                  # default false
  iso-27001: false             # default false

# ── Repo-type hint (overrides auto-detected hygiene gating) ───────────────────
repo:
  type: private-enterprise     # public-oss | private-enterprise | customer-delivery | internal-service | fork-research

# ── Custom taint: sources / sinks / sanitizers (appended to built-in 7-lang) ──
# Keyed by LANGUAGE; values are REGEX. Use single quotes so backslashes survive.
taint:
  sources:                     # framework/internal APIs returning attacker-controlled data
    java:
      - '\bHttpServletRequest\.getParameter\s*\('
    javascript:                # covers .js AND .ts
      - '\bctx\.request\.body\b'
  sinks:                       # dangerous APIs that must not receive tainted data
    java:
      - '\bTemplateEngine\.render\s*\('
  sanitizers:                  # escaping/validation that neutralises taint (any language)
    - '\bSecurityUtils\.escapeHtml\s*\('

# ── AI-authorship provenance (EU AI Act Art. 50 evidence — opt-in) ────────────
aiProvenance:
  enabled: false               # default false
  markers:                     # case-insensitive substrings in source comments
    - "AI-generated"
    - "Generated by GitHub Copilot"

# ── Baseline (report only NEW findings vs a saved report) ─────────────────────
baseline: null                 # default null — e.g. ".drift-baseline.json"
collapsePreexistingSecurity: false   # default false — in delta mode, collapse baseline
                                     # (pre-existing) security findings into a counted
                                     # PR-comment section; nothing leaves the full
                                     # report, SARIF, or the fail-on-severity gate

# ── Wasm hot-path (compute-heavy analysis acceleration) ───────────────────────
wasm:
  enabled: true                # default false — route supported analyzers through the
                               # bundled high-performance engine (degrades gracefully
                               # when unavailable)

# ── Plugins (explicit opt-in; sandboxed; no auto-discovery) ───────────────────
plugins: []                    # e.g. ["@acme/pullguard-rules"]

# ── Local vuln DB (offline / air-gapped CVE scanning) ─────────────────────────
db:
  path: "~/.drift-detector/db" # default ~/.drift-detector/db
  maxAgeDays: 7                # default 7 — warn if DB is staler
  sourceUrl: "https://osv-vulnerabilities.storage.googleapis.com"   # OSV mirror override

Baseline & delta

By default a scan reports the whole codebase. On a busy repo that means every pull request shows the same long list and developers stop reading it. Baseline and delta modes fix that — they show only what changed, so a PR comment reads “this change introduced 3 findings” instead of “the repo has 200”.

Three ways to scope output

ModeShowsHow
Full (default off-PR) The entire inventory. scan .  or  scan . --full
Baseline diff Only findings new vs a committed snapshot. scan . -b .drift-baseline.json
PR delta (“Clean as You Code”) Only findings the pull request introduced. scan . --deltaauto-on for PR events

Resolution order when more than one applies: an explicit --baseline <file> → the baseline: key in .driftrc.yml → an auto-detected .drift-baseline.json at the repo root. Opt out with --no-baseline. Critical and security findings always surface even in delta mode — an injection, secret, or RCE is never hidden just because it is “old” or sits in an unchanged file.

The .drift-baseline.json file

A fingerprinted snapshot of an accepted scan. Each finding carries a stable fingerprint that survives line-number shifts — an edit above a finding does not make it look “new”. Commit the file so every later scan can diff against it.

{
  "version": "1.3.4",
  "generatedAt": "2026-06-23T10:14:55.812Z",
  "projectPath": "/work",
  "score": 22,
  "grade": "B",
  "findingCount": 3,
  "findings": [
    {
      "fingerprint": "kx9p2a_87",
      "ruleId": "complexity",
      "type": "high_complexity",
      "file": "src/main/java/com/example/OrderReconciler.java",
      "severity": "moderate",
      "excerpt": "Function 'reconcile' has cyclomatic complexity 21 (threshold 15)"
    },
    {
      "fingerprint": "3mf0wq_112",
      "ruleId": "duplication",
      "type": "code_duplication",
      "file": "src/main/java/com/example/LegacyImporter.java",
      "severity": "minor",
      "excerpt": "Duplicated block (18 lines) also in BulkImporter.java"
    },
    {
      "fingerprint": "9aa7zk_64",
      "ruleId": "monolithic_file",
      "type": "monolithic_file",
      "file": "src/main/java/com/example/ContentService.java",
      "severity": "moderate",
      "excerpt": "File has 1240 lines (threshold 1000)"
    }
  ]
}

Generate & commit it

Recommended (CI): let the scan that already runs on your base branch maintain the baseline. Add update-baseline: true to a run triggered on a push to your integration branch — it writes .drift-baseline.json from that scan’s full inventory, automatically at the right tier, image, and context, so the baseline always matches what your PR scans compare against. No separate key-handling step:

# .github/workflows/pullguard-baseline.yml — runs on the base branch
on:
  push:
    branches: [develop]        # your integration branch
permissions:
  contents: write
jobs:
  baseline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: pullguard-dev/pullguard-action@v1
        with:
          license-key: ${{ secrets.PULLGUARD_LICENSE_KEY }}
          update-baseline: true
      # commit the refreshed baseline
      - run: |
          git config user.name  github-actions[bot]
          git config user.email 41898282+github-actions[bot]@users.noreply.github.com
          git add .drift-baseline.json
          git diff --cached --quiet || git commit -m "chore: refresh PullGuard baseline"
          git push

Your PR workflow needs no change — on a pull request the scan auto-detects the committed .drift-baseline.json and shows only new findings.

Alternative (one-off / local): the dedicated command writes the same file directly.

pullguard baseline . --license-key "$KEY"   # generate at your scan tier

# In Docker (offline / air-gapped form)
docker run --rm -v "$PWD:/work" -e PULLGUARD_LICENSE_KEY="$KEY" \
  --entrypoint node ghcr.io/pullguard-dev/pullguard:1 \
  /app/dist/bin/drift-detector.js baseline /work -o /work/.drift-baseline.json

git add .drift-baseline.json && git commit -m "chore: PullGuard baseline"

Refresh it the same way after your integration branch moves — regenerating is just a scan written to disk.

Generate the baseline at your scan tier, with the same image. A baseline is only valid for the engine and license tier that produced it. Pass your license (--license-key / PULLGUARD_LICENSE_KEY) and generate it with the same Action/image you scan with — otherwise the delta can’t match your scan’s findings and a PR shows a near-full report. Keep .drift-baseline.json tracked (not git-ignored) so CI can read it.

Security always surfaces. The delta hides pre-existing tech-debt on untouched files, but security findings appear on every PR by design. To keep a security-heavy repo’s PR view focused, set collapsePreexistingSecurity: true (or the collapse-preexisting-security Action input) — pre-existing security findings move to a collapsed, counted section while new ones stay inline; nothing is removed from the full report, SARIF, or the build gate.

What a delta scan prints

Every comparison prints a one-line summary; the report body then contains only the new findings:

Baseline comparison: 3 new, 5 fixed, 42 unchanged (delta: -2)

The committed history files that power finding ages and the over-time dashboard (.drift-history.json, .drift-scan-details.json, .drift-trend.json) are covered on the Reports & Dashboard page.

CLI reference

The Action wraps the same CLI shipped in the image. Run it directly as pullguard <command> (or node /app/dist/bin/drift-detector.js <command> inside the container). --help works on any command.

Commands

CommandWhat it does
scan [path]Analyze a project and output a report (text / json / sarif / markdown / html).
initCreate a starter .driftrc.yml.
baseline [path]Write a fingerprinted snapshot of current findings (default .drift-baseline.json).
dashboard [path]Render the self-contained over-time HTML dashboard. --org <dir> renders a portfolio view across many repos.
trendShow the compliance-score trend over time.
ignore <fingerprint>Add a finding to .pullguardignore (--status wontfix|false_positive|acknowledged).
list-rulesList every registered analyzer and its metadata.
import-semgrep <file>Convert the regex subset of a Semgrep ruleset into .driftrc.yml custom rules. Enterprise
db update|status|export|importManage the local CVE database for offline / air-gapped scanning.

scan options

FlagEffect
-f, --format <fmt>text | json | sarif | markdown | compliance | html (default text).
-o, --output <file>Write the report to a file instead of stdout.
-c, --config <path>Path to a .driftrc.yml (auto-detected at the root otherwise).
--min-severity <level>info | minor | moderate | major | critical.
-b, --baseline <file>Show only findings new vs a baseline (auto-detects .drift-baseline.json).
--no-baselineIgnore any committed baseline — show the full inventory.
--update-baselineWrite/refresh .drift-baseline.json from this scan (run a full scan on your base branch). Refuses on a PR/delta scan.
--delta / --fullForce PR-delta filtering / force full output (delta auto-on for PR events).
--blameAnnotate each finding with its git-blame “introduced” date.
--base-ref <ref>Git ref to diff against (default $GITHUB_BASE_REForigin/main).
--collapse-preexisting-securityIn delta mode, collapse pre-existing (baseline) security findings into a separate counted section of the PR comment — nothing is removed from the full report, SARIF, or the gate. See Baseline & delta.
--cost / --hourly-rate <n>Show a technical-debt cost estimate (default rate 150).
--input <file>Reformat an existing JSON report (skip scanning).
--license-key <key>License key (or set PULLGUARD_LICENSE_KEY).
--cacheIncremental analysis — cache results in .drift-cache.json and re-analyze only files changed since the last run.
--no-colorDisable colored output (the NO_COLOR environment variable is also respected).
-q, --quiet / -v, --verboseScore line only / analyzer timing + debug.

Full per-command help: pullguard scan --help.

.pullguardignore

Suppression file at the repo root for known-acceptable findings. File-glob patterns suppress entire paths; rule: entries suppress a specific rule globally or in a path.

Syntax

# Comments start with #

# Suppress all findings in vendored / generated paths
vendor/**
src/generated/**
**/*.gen.ts

# Suppress a specific rule everywhere
rule:builtin/complexity:cyclomatic-too-high

# Suppress a specific rule in a specific path
rule:builtin/duplication:type-1-clone src/legacy/**

# Suppress an analyzer's findings on one file
rule:builtin/dead-code:* src/api/public-surface.ts

Hard floor: security-category analyzers cannot be suppressed via rule: entries. The loader rejects any suppression targeting a security rule with a clear error. File-glob suppressions still apply (a path you do not scan is a path you do not scan), but a security rule cannot be silently ignored on a file that IS scanned.

Adding suppressions from a PR comment

Team Enterprise Comment /pullguard ignore <rule-id> on a PR. The PullGuard App opens a follow-up PR adding the entry to .pullguardignore with the original finding linked in the body for audit purposes. Reviewer of that follow-up PR is the auditor of record.

Tier limits

Each plan has a different scope. Tier limits are enforced at scan time via online validation; the scanner falls back to Free tier with a clear banner when a cap is hit, so a scan never silently breaks.

Plan Analyzers Repositories Contributors Enforcement
Free 14 Unlimited public; 1 private Unlimited Hard — analyzers gated client + server side
Pro 42 of 44 1 private (bound at first scan) Unlimited Hard — three-layer repo binding (Worker + scanner + Worker server-side)
Team All 44 Up to 10 private Unlimited Hard — repos accumulate on first scan; 11th repo runs Free until a slot is freed or the customer upgrades
Enterprise All 44 Unlimited Unlimited Contractual (no code-level cap)

Team-tier 10-repo cap behaviour

When a Team-tier license scans a repository, that repository is recorded against the license. The customer can scan up to 10 distinct repositories. Behaviour at the boundary:

No contributor cap

PullGuard does not cap contributors on any tier. A 5-developer team and a 50-developer team on the same set of repositories pay the same price. Repository count is the single tier dimension.

Air-gapped mode

For runners behind a firewall or with no outbound internet, PullGuard ships a local vulnerability database. Update it from a connected machine, transfer the archive across your air-gap, and run scans with no network calls.

1. Download the OSV mirror (connected machine)

docker run --rm -v "$PWD/db:/db" \
  ghcr.io/pullguard-dev/pullguard:latest \
  db update --path /db

2. Export to a portable archive

docker run --rm -v "$PWD/db:/db" -v "$PWD:/out" \
  ghcr.io/pullguard-dev/pullguard:latest \
  db export --path /db --to /out/pullguard-db.zip

3. Transfer + import on the air-gapped runner

docker run --rm -v "$PWD:/in" -v "$PWD/db:/db" \
  ghcr.io/pullguard-dev/pullguard:latest \
  db import --from /in/pullguard-db.zip --path /db

4. Point .driftrc.yml at the local DB

db:
  path: /opt/pullguard/db   # absolute path on the runner
  maxAgeDays: 30            # warn (do not fail) when DB exceeds this

With db.path set, no outbound calls to OSV are made. The archive is integrity-checked on import (SHA-256). On-disk files are created with restrictive permissions.

Secrets & permissions

Repository secrets

Secret name Required for Value
PULLGUARD_LICENSE_KEY Pro / Team / Enterprise Your pg_live_* token from Stripe checkout email.
GITHUB_TOKEN All tiers Auto-provided by GitHub Actions — you do not create this.

Workflow permissions

PullGuard needs the following permissions: block at the workflow or job level:

permissions:
  contents: read           # to read your code
  pull-requests: write     # to post the PR comment
  checks: write            # to write Check Runs (Enterprise: report-to-app)

If your organisation default permissions are restrictive (recommended), the block above is required. If your defaults are permissive (read-write), the workflow runs without an explicit block.

Repository-level vs organisation-level secrets

Repository-level is the CISO-friendly default — each repo gets its own secret, with explicit per-repo audit trail and no cross-repo blast radius. Organisation-level with a repo allowlist works for platform teams managing many repos. Both models are supported; the workflow file is identical.


← Getting Started PullGuard home →