← Back to PullGuard

Troubleshooting

Every advisory banner PullGuard prints, what it means, and what to do about it — plus how to tell a deliberate zero from a broken setup.

Start with pullguard doctor

Most "why didn't PullGuard do X?" questions are answered by the environment, not the code. doctor checks it in one command — read-only, and your license key is always masked in its output:

pullguard doctor .          # human-readable
pullguard doctor . --json   # machine-readable

It exits non-zero only when a check failed (a scan would run degraded), so you can use it as a CI setup gate.

License & tier banners

When a scan can't run at your paid tier it says so at the top of the report and degrades to the free tier for that one scan — your subscription is never affected by a failed check.

⚠️ "License check failed for this scan"

The banner names the cause. Network: your runner could not reach pullguard.dev/api/validate — check the runner's outbound HTTPS allowlist or firewall. Service error: usually a brief outage on our side; the next scan retries automatically. Persisting? hello@pullguard.dev.

⚠️ "License expired"

Scans run at the free tier until renewal. A warning appears in reports for the last 14 days before expiry so this never lands as a surprise mid-sprint.

⚠️ "Team-plan validation needs a repository identifier"

In GitHub Actions this is automatic. In other CI or local runs, set PULLGUARD_REPO=<org>/<repo> (or scan inside a git clone with an origin remote).

⚠️ "Team-plan repo cap reached"

The Team plan covers 10 distinct repositories; the 11th+ scans at the free tier while your existing repos are unaffected. Free a slot via hello@pullguard.dev or upgrade to Enterprise for unlimited repos.

ℹ️ "Free tier — this scan ran 14/46 analyzers"

Not an error — it's the honesty line. Deep security (taint tracking), supply-chain/CVE, AI-era and compliance checks are partial or absent on the free tier, and category grades reflect only the checks that ran. A clean free-tier scan is not a security clean bill.

"Was my whole repo scanned?" — coverage caps

Large repositories can exceed the default scan caps. PullGuard never truncates silently: the report's scan-coverage note states exactly how many collected files were never read, and pullguard doctor predicts it before you scan. To widen coverage, raise the caps in .driftrc.yml:

maxFiles: 10000       # collection cap (traversal)
maxFilesRead: 10000   # read cap — only read files are analyzed
maxDepth: 20          # directory depth

On a very large monorepo, scanning per-package usually beats one giant scan. Full reference: Configuration.

"The scan hangs on one file"

Very occasionally a single file — usually a generated, minified or otherwise pathological one — makes one analyzer take far longer than the rest of the repo put together. First, confirm it is one file: re-run with that path excluded.

exclude:
  - "**/vendor/**/*.min.js"     # the usual suspects: generated + minified files

If you would rather PullGuard handle it for you, turn on worker isolation. Each analyzer then runs in its own worker thread with a per-analyzer time limit: one that overruns is stopped, and the scan finishes with the rest of the results. It is opt-in because it is a different execution path — the findings are identical either way, and it costs a little start-up time per scan.

analyzerIsolation: worker       # .driftrc.yml — or PULLGUARD_ANALYZER_ISOLATION=worker for one run

The per-analyzer limit defaults to 180 seconds. If an analyzer is stopped that you believe was doing real work (very large repositories can legitimately need longer), raise it with PULLGUARD_ANALYZER_TIMEOUT_MS — it is clamped to 5–600 seconds, so the limit can never be switched off entirely.

A stopped analyzer is never hidden: the report lists it under incompleteAnalyzers, the PR comment carries the partial-scan banner, and the absence of its findings must not be read as "clean". pullguard doctor tells you which mode a repo resolves to, and warns if worker was asked for but is unavailable (in which case the scan runs inline).

"A finding I expected didn't fire"

Work through these in order — in our support history they explain nearly every case:

  1. Was the analyzer running at all? Most security analyzers are paid-tier. pullguard doctor shows the resolved tier and analyzer count for the exact environment your scan uses.
  2. Was the file read? Check the report's scan-coverage note (or doctor's coverage check) — an unread file is an unchecked file.
  3. Is it a deliberate precision gate? Many rules require corroborating context before firing, so the same line can be a true positive in one file and correctly silent in another. That's calibrated precision, not a miss.
  4. Verify with a planted control. On a throwaway branch, add a knowingly-detectable snippet of the same class next to your real case and scan. If the control fires and your case doesn't, the rule is running and judged your code differently — worth a report to hello@pullguard.dev with both snippets; that's exactly the evidence we act on fastest.

Air-gapped & offline runs

PULLGUARD_OFFLINE=true is the one switch: every external call stays local (signed embedded rule fallbacks, local CVE database, skipped freshness sweeps). Online pg_live_ tokens cannot validate without network — air-gapped deployments use offline keys instead. Details and the full egress disclosure: Deployment & data sovereignty.

Windows runners

Windows checkouts (CRLF line endings) are first-class: our detection suites run every fixture under both LF and CRLF, so results do not differ by checkout platform.

Using the Action's outputs

The Action exposes three outputs per scan — score (0–100, lower is better), grade (A–F), and findings (count) — usable in later workflow steps:

- name: PullGuard
  id: pullguard
  uses: pullguard-dev/pullguard-action@v1

- name: Use the result
  run: echo "Grade ${{ steps.pullguard.outputs.grade }} (score ${{ steps.pullguard.outputs.score }}, ${{ steps.pullguard.outputs.findings }} findings)"

The full report lands in three places per run: the PR comment, the workflow's Step Summary, and the pullguard-report artifact (self-contained HTML report + over-time dashboard + SARIF). Findings past their configured age budget are flagged via the sla config block — see Configuration.

Still stuck?

Email hello@pullguard.dev — attaching the output of pullguard doctor (keys are masked automatically) answers most questions in one round trip.


← PullGuard home Configuration reference →