Shared abuse intelligence for suspicious GitHub pull request activity.
A single GitHub App + public directory that helps maintainers spot bounty-farming, AI-spam, low-effort duplicate PRs, and outright malicious contributions before they waste anyone's time.
When you install the OSS Protector GitHub App on a repository, it:
- Watches PRs. Every
pull_request,issue_comment, andpull_request_review_commentevent is sent to a shared webhook. - Analyzes the PR. Changed files, patch snippets, and metadata are inspected for known abuse patterns — fake bounty farming, duplicate low-effort PRs, AI-filler, credential phishing, malicious code, dependency-script abuse, obfuscation, and backdoor indicators.
- Scores the contributor. A scoring engine combines signals from this PR, prior reports across all installed repos, reporter trust, and age decay. AI validation (via OpenRouter) sanity-checks the result; deterministic fallback runs when no API key is configured.
- Comments. Strong evidence gets posted as a PR assessment. Weaker signals stay in a review queue. Maintainers can confirm, dismiss, allow, or reset with
@oss-protectorcommands. - Publishes. Confirmed risky accounts show up on the public directory so other maintainers can review them before merging.
It's one GitHub App, one database, one feed — maintainers don't each have to run their own.
- Maintainer-first review lifecycle — PR webhooks are tracked, external contributors are reviewed, maintainers can correct outcomes from PR comments, and public scores update from validated evidence.
- Grounded abuse signals — lifecycle-script execution, token exfiltration, obfuscation, privileged
pull_request_targetworkflow patterns, duplicate cross-repo campaigns, and maintainer reports are handled separately. - False-positive guardrails — repo insiders are skipped, non-maintainer reports stay in review until a maintainer confirms them, command-only reports are capped, and harmless docs about webhooks/secrets are not treated as credential phishing.
- Repo-local policy — projects can add
.github/oss-protector.jsonto disable analysis, trust local automation accounts, ignore path-only changes, or raise the confidence threshold for likely-abuse results. - Auditable profiles — public profile pages show recent public PRs, reports, and a decision timeline of the signals that affected the score while hiding private-repo source links.
- Frontend — TanStack Start (file-based routing, SSR), React 19, shadcn/ui on Base UI primitives, Tailwind 4.
- API + Worker — TanStack Start server functions on Cloudflare Workers.
- Data — Drizzle ORM
1.0.0-beta.24on Cloudflare D1. - Auth — Better Auth (GitHub user sign-in) +
@octokit/auth-app(App installation tokens). - AI — OpenRouter chat completions, free-tier model chain with a paid fallback.
- Lint/format — Ultracite (oxlint + oxfmt), Biome.
- Tests — Vitest.
git clone https://github.com/lord007tn/oss-protector.git
cd oss-protector
pnpm install
cp .env.example .env
pnpm devOpen http://localhost:3000. Most UI and scoring work can be done without D1 or a GitHub App. Database-backed directory data stays empty until D1 is configured or seeded.
For full Worker + D1 testing locally:
pnpm build
pnpm exec wrangler dev --local --port 8787
pnpm run db:migrate:local
pnpm run db:seedRequires Node 20+, pnpm 10, and a Cloudflare account for the Worker preview.
Copy .env.example to .env and fill what you need. None of the GitHub or OpenRouter values are required to run pnpm dev.
| Variable | Required for | Description |
|---|---|---|
VITE_APP_URL |
always | Public origin. Defaults to http://localhost:3000. |
VITE_ENABLE_GITHUB_AUTH |
UI sign-in | Set to true to enable the GitHub login button. |
VITE_ENABLE_DEVTOOLS |
local debugging | Set to true to enable TanStack, React Query, and React Scan devtools in development. |
ALLOW_UNSIGNED_GITHUB_WEBHOOKS |
local webhook testing | Keep false outside local development. Localhost can still run unsigned when no webhook secret is configured. |
CLOUDFLARE_ACCOUNT_ID |
deploy / D1 | Required by Wrangler when your Cloudflare login has access to more than one account. |
CLOUDFLARE_D1_DATABASE_NAME |
D1 | Defaults to oss-protector. |
CLOUDFLARE_D1_DATABASE_ID |
self-hosted D1 | Optional override for the committed hosted D1 UUID. |
CLOUDFLARE_D1_TOKEN |
Drizzle Kit | API token for drizzle-kit push against remote D1, if you use that workflow. |
VITE_GITHUB_STARS |
build | Optional override for the generated GitHub star count. |
VITE_GITHUB_REPO_SLUG |
build | Optional owner/repo override for the generated GitHub star count. |
BETTER_AUTH_SECRET |
sign-in | Required to enable Better Auth sessions. Must be ≥ 32 bytes — generate with openssl rand -base64 32. Also used to derive the BYOK encryption key. |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
sign-in | GitHub OAuth credentials for Better Auth. |
VITE_ENABLE_EMAIL_OTP |
UI sign-in | Set to true to show the email OTP option alongside the GitHub button. |
RESEND_API_KEY |
email OTP delivery | Resend API key. If unset, OTP codes are logged to the server console in local dev; in non-localhost the send is rejected with a clear error. |
EMAIL_FROM |
email OTP delivery | From address for OTP emails. Production must use a Resend-verified domain. Defaults to OSS Protector <onboarding@resend.dev>. |
GITHUB_APP_ID / GITHUB_APP_PRIVATE_KEY |
webhooks | The GitHub App's identity, used for installation tokens. |
GITHUB_APP_SLUG / VITE_GITHUB_APP_SLUG |
server / UI | GitHub App slug. Set both for self-hosted installs so webhook code and browser install links point at the same app. |
GITHUB_MANIFEST_TOKEN |
GitHub App setup | Optional token used to exchange a GitHub App manifest code from /install. |
GITHUB_APP_CREATE_OWNER |
GitHub App setup | Optional owner slug for GitHub App manifest creation. |
GITHUB_WEBHOOK_SECRET |
webhooks | Verifies inbound webhook signatures. |
OPENROUTER_API_KEY |
AI scoring | If unset, the deterministic fallback runs. |
SMOKE_HEALTH_TOKEN |
deploy smoke | Bearer token required by the private post-deploy webhook health endpoint. |
Other projects can query the directory through filterable JSON endpoints (see /api-docs for the full reference):
/api/accounts— risky accounts with status / score / reason / search filters./api/protectors— maintainers who submitted review signals./api/openrouter/free-models— the model IDs the platform key cycles through (for transparency).
Both filterable endpoints are rate-limited per client IP (60 req/min) via the Cloudflare Rate Limiting binding configured in wrangler.json. Webhooks are not throttled.
Authenticated maintainer endpoints (require a Better Auth session cookie, not rate-limited):
GET / POST /api/user/preferences— read and update your notification kinds + BYOK OpenRouter key.POST /api/openrouter/test— validate a BYOK key against OpenRouter without consuming model credit.POST / DELETE /api/maintainer/repo-decision,GET /api/maintainer/repo-decisions— per-repo allow/block decisions for a specific account.GET / POST / DELETE /api/maintainer/repo-policy?repositoryId=…— dashboard-saved repository policy that fills in for fields not set by the committed.github/oss-protector.json.
Schema lives in src/db/schema.ts and is the single source of truth. Migrations
are generated from it with Drizzle Kit and applied with drizzle-kit migrate.
pnpm run db:generate # generate a migration after editing schema.ts
pnpm run db:migrate:local # apply pending migrations to the local D1
pnpm run db:migrate # apply pending migrations to the remote (prod) D1
pnpm run db:seed # seed locally (db:seed:remote for prod)Local maintenance (these never touch prod unless you add --remote --yes):
pnpm run db:reset # wipe all rows, keep the schema
pnpm run db:clean # drop every table (then db:migrate:local to rebuild)Deploys apply migrations first. On a push to master, migrations run before
the Worker is built and deployed, so the schema is always ahead of the code:
- GitHub Actions:
.github/workflows/deploy.ymlrunsdb:migrate→ build → deploy. - Cloudflare Workers Builds (Git integration): set the build command to
pnpm run cf:build, which runsdb:migratebeforebuild:prod.
One-time production baseline. The prod database was created before this Drizzle-tracked flow, so its tables already exist. Run the baseline once to record the current migration as applied without re-running it — otherwise the first deploy would try to re-create existing tables:
pnpm run db:baseline:remote # mark current migrations applied on prod, runs no SQLAfter baselining, every future change is just: edit schema.ts →
pnpm db:generate → push. The deploy applies it automatically.
The seed imports Bounty-Hunters/clankers.json. The initial dataset and the original concept are credited to the Clankers Leaderboard by @heyandras.
OSS Protector is one shared GitHub App. Maintainers don't create their own — they install the shared app on the repos they own:
https://github.com/apps/oss-protector/installations/new
If you're self-hosting your own instance, create a GitHub App with:
Webhook URL: https://<your-worker-host>/api/github/webhook
Repository permissions: Contents read, Issues write, Pull requests write
Subscribed events: Issue comment, Pull request, Pull request review comment
Visibility: Public
Then store the secrets in Cloudflare:
wrangler secret put GITHUB_APP_ID
wrangler secret put GITHUB_APP_PRIVATE_KEY
wrangler secret put GITHUB_WEBHOOK_SECRET
wrangler secret put BETTER_AUTH_SECRET
wrangler secret put GITHUB_CLIENT_ID
wrangler secret put GITHUB_CLIENT_SECRET
wrangler secret put OPENROUTER_API_KEY
wrangler secret put SMOKE_HEALTH_TOKENBetter Auth handles GitHub user sign-in at /api/auth/* once BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, and GITHUB_CLIENT_SECRET are set. The webhook + installation-token flow still uses @octokit/auth-app — Better Auth does not replace GitHub App installation authentication.
The internal OpenRouter model chain only uses model IDs that end in :free, with a paid fallback when free-tier models hallucinate.
Once signed in at /login, every maintainer gets /dashboard with these tabs:
- Review queue — pending and needs-review reports from your repos.
- Appeals — flagged accounts who submitted an appeal via
/appeal. Uphold / reject from here. - Audit log — unified timeline of reports + maintainer corrections + repo overrides, filter by
all/decisions/overrides/reports. - Coverage — every repo where OSS Protector is installed and active.
- Allowlist — accounts you explicitly trust; their PRs bypass the analyzer.
- Repo overrides — per-repo allow/block for a specific account, independent of the shared score. Allow short-circuits AI review; block synthesizes a high-confidence flag without consuming AI credit.
- Repo policy — form-based editor for the same fields as
.github/oss-protector.json. The committed file always wins per-field; the DB-saved policy fills in where the file is silent.
The /settings page lets you toggle which notification kinds you receive (report, dispute, flag, correction, ok, info — muted kinds aren't even created) and bring your own OpenRouter key for AI scoring. The key is encrypted at rest with AES-256-GCM (HKDF off BETTER_AUTH_SECRET); the earliest-linked maintainer's key wins on multi-maintainer installs. Without a BYOK key, the platform-provided key runs the free-model chain only.
Each repository can tune OSS Protector with an optional .github/oss-protector.json file, or by editing the Repo policy tab in the dashboard. The committed file always takes precedence per-field; the dashboard value fills in for any field the file doesn't set. See Repository policy for the full lifecycle, examples, and field reference.
{
"enabled": true,
"analyzePrivateRepositories": false,
"minimumLikelyAbuseConfidence": 80,
"trustedAuthors": ["dependabot[bot]", "renovate[bot]"],
"ignoredPaths": ["docs/", "examples/"]
}enabled: falsetracks PR metadata but skips automatic abuse review for that repo.analyzePrivateRepositories: trueexplicitly opts private repositories into third-party AI analysis. Private repos default to metadata tracking without OpenRouter review.minimumLikelyAbuseConfidenceis clamped between65and95; lower-confidence likely-abuse findings become review-needed.trustedAuthorsskips automatic review for known local automation accounts.ignoredPathsskips automatic review when every changed file starts with one of the configured prefixes.
Anyone can mention the bot to file a report:
@oss-protector review this user
@oss-protector flag this user reason: fake bounty
@oss-protector recommend block reason: malicious code
Repo owners, organization members, and collaborators (GitHub author_association of OWNER, MEMBER, or COLLABORATOR) can correct the system from any PR comment:
@oss-protector dismiss # mark all open reports on this PR's author as dismissed
@oss-protector confirm # validate the most recent open report
@oss-protector allow # allowlist the PR author (status = allow, score = 0)
@oss-protector reset # clear a prior allowlist; score recomputes on the next webhook
The bot posts a confirmation comment for each correction. Non-maintainer comments using those verbs are ignored. Cross-target syntax (@oss-protector allow @other-user) is not supported — corrections always act on the PR author. The bot flags any cross-target attempt in its ack comment.
pnpm check # ultracite (oxlint + oxfmt)
pnpm run typecheck # tsc --noEmit
pnpm test # vitest
pnpm build # vite buildCI runs the same chain on pushes to master and PRs targeting master.
The hosted Worker is bound to the oss-protector D1 database in wrangler.json. Self-hosted deploys must set CLOUDFLARE_D1_DATABASE_ID, update the Worker name and public URL in wrangler.json, and store their own GitHub App/OpenRouter secrets before deploying. The deploy script refuses to publish the hosted configuration unless OSS_PROTECTOR_DEPLOY_TARGET=hosted is set.
pnpm run deployDeploys are wired through Cloudflare's Git integration on the hosted instance — every push to master triggers a Cloudflare-managed build and deploy. Maintainers of the hosted instance can run pnpm run deploy:hosted locally only for out-of-cycle hotfixes.
PRs and bug reports are very welcome. See CONTRIBUTING.md for setup and workflow, and CODE_OF_CONDUCT.md for community expectations.
Found a security issue? Please follow SECURITY.md and do not open a public issue.
MIT © OSS Protector contributors.