What your team sees on every PR
VibeSniffer posts a clear, actionable summary directly in the PR conversation.
Setup in 2 minutes
Three steps. No API keys. No signup required.
Create the workflow file
Create a new file at .github/workflows/vibesniffer.yml in your repository. Copy the YAML below.
Commit and push
Commit the workflow file to your main branch. GitHub Actions will pick it up automatically.
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
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 1Frequently 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.