Enforcement is hybrid: a GitHub App webhook check-runner (zero consumer setup) plus a GitHub Action (for repos that want it in their own workflow). Both call the same brain — checkCompliance in the worker — so probing /v1/check audits every path at once.

The App check-runner (primary path)

Key behaviours:
  • 1–2 minutes of latency is normal — evaluation never runs in the webhook request (LLM calls exceed the waitUntil budget; in_progress check runs used to orphan as eternal spinners, hence complete-on-create).
  • The webhook maps installation_id → org via codex.github_installations. An unclaimed installation is dropped with a warning log — see troubleshooting.
  • Default-branch pushes are also checked (compare base = push range / first parent).
  • The checks-UI Re-run button works: check_run/check_suite rerequested re-enqueues.
  • Merge-blocking comes from a repo ruleset requiring the PolicyCodex check pinned to App integration id 4542396. On GitHub’s free plan that only works on public repos or Team-plan orgs.

Evidence modes (a real bug class)

checkCompliance has two system prompts selected by evidence mode: This split exists because a model given file contents under the diff-framed prompt audits “the change”, sees none, and correctly reports nothing (observed live: gpt-4.1 flagged the same violating line 3/3 as a diff and 0/3 as file contents before the fix). If scans stop finding known-bad content, check this seam first. Model quality is load-bearing. Per-org Settings → LLM overrides the fallback; the model must honor OpenAI-style response_format: json_object (Anthropic models via OpenRouter do not, and fail the JSON parse). openai/gpt-4.1 is the reference model validated by the eval suite.

Full-repo scans

/v1/scan (REST, API key) or Reporting → Scope’s per-repo button (request_full_scan RPC) queue a row in pending_scans; the same cron sweep evaluates it — whole file contents (capped: 400 files / 55k chars, .md .ts .tsx .js .jsx .py .go .rb .java etc.), one LLM call, scan evidence mode. Guards: a per-repo cooldown (429 “scanned too recently”), one queued scan per repo/domain, and the github_scanning entitlement checked at execution time.

Violations lifecycle

  • Created by check runs and scans (severity: blocking for enforced MUST, warning for SHOULD), status='open'.
  • Auto-resolve on re-evaluation of the same scope: a later clean run for the same repo/PR supersedes old findings (status='superseded'); closing a PR resolves its findings (pr_closed). Nothing resolves by hand-editing rows.
  • Reporting reads violations plus the violations.recorded/violations.resolved audit events.

The GitHub Action (secondary path)

For consumers who want the check inside their own workflow: inputs api-base-url, api-key (a per-consumer key minted in Settings → API keys), optional domain. It fetches enforced statements, POSTs the diff to /v1/check, posts a PR review with findings (statement text, slug, link, severity), fails on enforced MUST findings, and POSTs violations back. The bundle is apps/action/dist/index.cjscommitted, so rebuilding without committing ships nothing. Private-repo consumers must check out the policycodex repo with a fine-grained PAT and uses: the local path.
Settings → Integrations: GitHub App connection status for the organisation