Add VibeSniffer to your CI/CD pipeline

Scan every pull request for security issues automatically. One YAML file, zero configuration, instant protection.

Automatic Security Scanning

Every PR is scanned for secrets, SQL injection, XSS, command injection, and 20+ vulnerability patterns.

PR Comments with Results

VibeSniffer posts a detailed comment on every PR with findings, grades, and actionable fix suggestions.

Fail on Bad Grades

PRs with a D or F grade automatically fail the check, preventing insecure code from being merged.

Zero Configuration

Just drop in the workflow file. No API keys, no tokens, no setup. Works with any public repo instantly.

What your team sees on every PR

VibeSniffer posts a clear, actionable summary directly in the PR conversation.

VS
vibesnifferbot

🟢 VibeSniffer Scan: Grade B (82/100)

CriticalWarningsInfoTotal
0325

🟔 src/api/auth.ts:42 Request data used without input validation

🟔 src/utils/db.ts:18 HTTP URL used instead of HTTPS

🟔 package.json:1 No rate limiting detected

Scanned by VibeSniffer — free security scanner for vibe coders

Setup in 2 minutes

Three steps. No API keys. No signup required.

1

Create the workflow file

Create a new file at .github/workflows/vibesniffer.yml in your repository. Copy the YAML below.

2

Commit and push

Commit the workflow file to your main branch. GitHub Actions will pick it up automatically.

3

Open a PR

Open any pull request. VibeSniffer will scan the repo, post results as a comment, and set the check status.

.github/workflows/vibesniffer.yml
# .github/workflows/vibesniffer.yml
name: VibeSniffer Scan

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  pull-requests: write

jobs:
  vibesniffer:
    name: VibeSniffer Security Scan
    runs-on: ubuntu-latest
    steps:
      - name: Run VibeSniffer scan
        id: scan
        run: |
          REPO_URL="https://github.com/${{ github.repository }}"

          RESPONSE=$(curl -s -w "\n%{http_code}" \
            -X POST "https://vibesniffer.com/api/ci" \
            -H "Content-Type: application/json" \
            -d "{\"repoUrl\": \"${REPO_URL}\"}")

          HTTP_CODE=$(echo "$RESPONSE" | tail -1)
          BODY=$(echo "$RESPONSE" | sed '$d')

          if [ "$HTTP_CODE" != "200" ]; then
            echo "VibeSniffer scan failed (HTTP $HTTP_CODE)"
            echo "$BODY"
            exit 1
          fi

          GRADE=$(echo "$BODY" | jq -r '.grade')
          SCORE=$(echo "$BODY" | jq -r '.score')
          CRITICAL=$(echo "$BODY" | jq -r '.summary.critical')
          WARNINGS=$(echo "$BODY" | jq -r '.summary.warnings')
          INFO=$(echo "$BODY" | jq -r '.summary.info')
          TOTAL=$(echo "$BODY" | jq -r '.summary.total')
          SCAN_ID=$(echo "$BODY" | jq -r '.id')

          echo "grade=$GRADE" >> $GITHUB_OUTPUT
          echo "score=$SCORE" >> $GITHUB_OUTPUT
          echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
          echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
          echo "info=$INFO" >> $GITHUB_OUTPUT
          echo "total=$TOTAL" >> $GITHUB_OUTPUT
          echo "scan_id=$SCAN_ID" >> $GITHUB_OUTPUT

          FINDINGS_MD=""
          FINDING_COUNT=$(echo "$BODY" | jq '.findings | length')

          if [ "$FINDING_COUNT" -gt 0 ]; then
            FINDINGS_MD=$(echo "$BODY" | jq -r '
              .findings[:20] | to_entries[] |
              "| " + (
                if .value.severity == "critical" then "šŸ”“"
                elif .value.severity == "warning" then "🟔"
                else "šŸ”µ" end
              ) + " | \`" + .value.file + ":" + (.value.line | tostring) + "\` | " + .value.title + " |"
            ')
          fi

          {
            echo "findings_md<<VIBESNIFFER_EOF"
            echo "$FINDINGS_MD"
            echo "VIBESNIFFER_EOF"
          } >> $GITHUB_OUTPUT

      - name: Post PR comment
        uses: actions/github-script@v7
        with:
          script: |
            const grade = '${{ steps.scan.outputs.grade }}';
            const score = '${{ steps.scan.outputs.score }}';
            const critical = '${{ steps.scan.outputs.critical }}';
            const warnings = '${{ steps.scan.outputs.warnings }}';
            const info = '${{ steps.scan.outputs.info }}';
            const total = '${{ steps.scan.outputs.total }}';
            const findingsMd = `${{ steps.scan.outputs.findings_md }}`;

            const gradeEmoji = {
              'A': '🟢', 'B': '🟢', 'C': '🟔', 'D': 'šŸ”“', 'F': 'šŸ”“'
            };

            let body = `## ${gradeEmoji[grade] || '⚪'} VibeSniffer Scan: Grade ${grade} (${score}/100)\n\n`;
            body += '| Critical | Warnings | Info | Total |\n';
            body += '|----------|----------|------|-------|\n';
            body += `| šŸ”“ ${critical} | 🟔 ${warnings} | šŸ”µ ${info} | ${total} |\n\n`;

            if (findingsMd && findingsMd.trim()) {
              body += '### Findings\n\n';
              body += '| Severity | Location | Issue |\n';
              body += '|----------|----------|-------|\n';
              body += findingsMd + '\n\n';
              if (parseInt(total) > 20) {
                body += `> Showing top 20 of ${total} findings.\n\n`;
              }
            } else {
              body += '✨ No issues found!\n\n';
            }

            if (grade === 'D' || grade === 'F') {
              body += `> āš ļø **Grade ${grade}.** Review findings before merging.\n\n`;
            }

            body += '---\n🐺 Scanned by [VibeSniffer](https://vibesniffer.com)';

            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });

            const existing = comments.find(c =>
              c.body && c.body.includes('VibeSniffer Scan:')
            );

            if (existing) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: existing.id,
                body,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body,
              });
            }

      - name: Fail on bad grade
        if: ${{ steps.scan.outputs.grade == 'D' || steps.scan.outputs.grade == 'F' }}
        run: |
          echo "āŒ VibeSniffer grade: ${{ steps.scan.outputs.grade }}"
          exit 1

Frequently asked questions

Does this work with private repos?

Currently VibeSniffer scans public repos only. Private repo support is coming soon.

Do I need an API key?

Nope. The CI endpoint is free and requires no authentication. Just add the workflow file and you're done.

Will this slow down my CI?

Scans typically take 5-15 seconds. The workflow runs in parallel with your other checks, so it won't block anything.

What does it scan for?

Leaked secrets, SQL injection, XSS, command injection, eval usage, missing input validation, vulnerable dependencies, and 20+ other patterns.

Can I customize the fail threshold?

By default it fails on D or F grades. You can edit the workflow YAML to change the condition in the 'Fail on bad grade' step.

Is my code stored?

No. The CI endpoint downloads your code, scans it in memory, and deletes everything immediately. Nothing is saved to any database.

Ship secure code on every PR

Add VibeSniffer to your pipeline in under 2 minutes. Every repo using it shows the VibeSniffer badge — help your friends ship safer code too.