Detailed guide to invoking /vbs-scan-security, choosing scopes, understanding reports, and integrating into CI/CD.
Haven't installed the skill yet? Read installation.md first.
- Basic command
- All supported scopes
- Where reports are saved
- Choosing output language
- Anatomy of a report
- JSON summary for tooling
- Performance expectations
- Resuming after LARGE mode is interrupted
- CI/CD integration
Open Claude Code inside your git repo. In the chat input:
/vbs-scan-security
⚠️ v0.3 behavior change:/vbs-scan-security(no arguments) now scans the entire repository. Previously this was equivalent touncommitted. If you want the old behavior, use/vbs-scan-security uncommittedor/vbs-scan-security diff.
By default vbsec will:
- Collect every file in the repo (full tree, not just uncommitted)
- Detect the primary language (Go/PHP/JS/Python/...)
- Route to SMALL or LARGE mode based on file count
- Apply 21 generic rules + language overlay (if available)
- Print a Markdown report + JSON summary to stdout
- Save a copy of the report to
vbsec-reports/scan-<timestamp>.mdin the repo
| Command | What it scans |
|---|---|
/vbs-scan-security |
Entire repo (default — changed in v0.3) |
/vbs-scan-security uncommitted |
Only uncommitted changes (staged + unstaged) |
/vbs-scan-security diff |
Alias for uncommitted |
/vbs-scan-security staged |
Only staged files |
/vbs-scan-security commit within 7days |
Files touched in the last N days |
/vbs-scan-security commit id <sha> |
Files in a specific commit |
/vbs-scan-security pr id 42 |
Files in PR #42 (GitHub) — needs gh CLI |
/vbs-scan-security all |
Explicit alias for entire repo |
# Default — scan the whole repo, report in Vietnamese, saved to file
/vbs-scan-security
# Before committing: scan only what changed
/vbs-scan-security uncommitted
# Pre-merge review: scan a PR with English report
/vbs-scan-security pr id 42 lang=en
# Periodic audit: scan recent commits
/vbs-scan-security commit within 30days
# Find the latest report
ls -lt vbsec-reports/ | head -5
# Diff two scans
diff vbsec-reports/scan-2026-05-13-100000.md vbsec-reports/scan-2026-05-14-100000.mdFrom v0.3 onwards, every scan saves a copy of the report to a file in the scanned repo:
- Path:
vbsec-reports/scan-<YYYY-MM-DD-HHMMSS>.md - File naming format:
scan-YYYY-MM-DD-HHMMSS.md— sorts chronologically by default - The content is identical to what's printed to stdout — the file is for re-reading and sharing, stdout is for the immediate view
- If
vbsec-reports/is not in.gitignore, vbsec prints a recommendation at the end of stdout — vbsec does not auto-modify.gitignore, that's your decision - Reports can be deleted any time; they're not state files
Find the latest scan:
ls -lt vbsec-reports/ | head -5Show the most recent report:
cat "$(ls -t vbsec-reports/scan-*.md | head -1)"See installation.md for how to add vbsec-reports/ to .gitignore.
vbsec supports Vietnamese (default) and English. How to specify:
| Syntax | Result |
|---|---|
| (no flag) | Vietnamese |
lang=vi |
Vietnamese |
--vi |
Vietnamese |
lang=en |
English |
--en |
English |
Examples:
/vbs-scan-security pr id 42 lang=en
/vbs-scan-security commit within 14days --en
/vbs-scan-security staged
Note: The Markdown report changes by lang, BUT:
- Rule IDs (HARDCODED-SECRET, SQL-INJECTION...) are always EN
- File paths and code snippets stay verbatim
- The JSON summary at the end is ALWAYS EN canonical (so tooling can parse reliably)
A typical report:
# vbsec Security Scan Report
Scope: Uncommitted changes
Files: 12 (8 .ts, 4 .py)
Primary language: typescript
Mode: SMALL (inline scan)
Date: 2026-05-13
## VERDICT: FAIL
CRITICAL issues found. DO NOT deploy until fixed.
## CRITICAL (blocks deploy)
| File:Line | Rule | Issue | Fix |
|---|---|---|---|
| api/users.ts:42 | SQL-INJECTION | req.body.id concatenated into SQL | Use parameterized query |
| .env:1 | HARDCODED-SECRET | STRIPE_KEY=sk_live_... | Rotate key + .gitignore |
## HIGH (must fix)
| auth.ts:18 | WEAK-PASSWORD-HASHING | createHash('md5') | Use bcrypt/argon2 |
## MEDIUM
(none)
## PASSED CHECKS
- ✓ XSS — Vue templates auto-escape
- ✓ CORS — specific origin
- ✓ CSRF — token present on forms
## JSON Summary
```json
{
"verdict": "FAIL",
"scope": "uncommitted",
"files_scanned": 12,
"primary_language": "typescript",
"mode": "SMALL",
"counts": {"critical": 2, "high": 1, "medium": 0, "low": 0},
"findings": [
{"file": "api/users.ts", "line": 42, "rule": "SQL-INJECTION", "severity": "CRITICAL"},
{"file": ".env", "line": 1, "rule": "HARDCODED-SECRET", "severity": "CRITICAL"},
{"file": "auth.ts", "line": 18, "rule": "WEAK-PASSWORD-HASHING", "severity": "HIGH"}
]
}
| Section | Purpose |
|---|---|
| Header | Scope scanned, file count, primary language, mode (SMALL/LARGE), date |
| VERDICT | PASS / WARN / FAIL — decides whether you can deploy |
| CRITICAL | Blocks deploy, fix first |
| HIGH | Must fix before production |
| MEDIUM | Fix soon, doesn't block |
| PASSED CHECKS | Rules verified clean — proof of full coverage |
| JSON Summary | Machine-readable, for CI parsing |
| Condition | Verdict | Meaning |
|---|---|---|
| ≥1 CRITICAL | FAIL | Do not deploy |
| 0 CRITICAL, ≥1 HIGH | WARN | Can deploy, but plan a fix |
| 0 CRITICAL, 0 HIGH | PASS | OK to deploy |
WARN is not approval. Security/tech lead should still review HIGH issues.
The JSON summary always sits at the end of the report, in a json fenced code block. Schema is stable:
{
"verdict": "PASS" | "WARN" | "FAIL",
"scope": "uncommitted" | "staged" | "commit_within_Ndays" | "commit_id_<sha>" | "pr_<num>" | "all",
"files_scanned": <int>,
"primary_language": <string>,
"mode": "SMALL" | "LARGE",
"counts": {
"critical": <int>,
"high": <int>,
"medium": <int>,
"low": <int>
},
"findings": [
{
"file": <string>,
"line": <int>,
"rule": <string>,
"severity": "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"
}
]
}The report is Markdown — extract JSON with:
# Report saved to report.md
awk '/^```json$/,/^```$/' report.md | sed '1d;$d' > summary.json
jq '.verdict' summary.json
# "FAIL"Or in Node.js:
const fs = require('fs');
const md = fs.readFileSync('report.md', 'utf8');
const match = md.match(/```json\n([\s\S]*?)\n```/);
const summary = JSON.parse(match[1]);
if (summary.verdict === 'FAIL') process.exit(1);| Mode | Trigger | Time | Resources |
|---|---|---|---|
| SMALL | ≤20 primary-lang files AND ≤30 total AND ≤14 days | 30-60 seconds | 1 agent, ~5-15 tool calls |
| LARGE | Any of the three thresholds exceeded | 5-15 minutes | Parallel sub-agents, one per chunk |
LARGE mode uses chunking-strategy.md to split files by top-level folder, with one sub-agent per chunk. The main agent aggregates findings + translates at the end.
You don't have to think about it — SKILL.md routes automatically. But if you want to force:
- Narrow scope (
staged,commit id) → usually SMALL - Wide scope (
all,commit within 30days) → LARGE - Small repo (<30 files) → always SMALL even with
all
LARGE mode uses TodoWrite to track each chunk. If you interrupt mid-run (Ctrl+C, crash, etc.):
- Reopen Claude Code in the same directory
- Re-run the original command (same scope, same lang)
- The skill sees the existing
.vbsec-tmp/directory with prior findings and resumes from the unfinished chunk
Important:
- Don't manually delete
.vbsec-tmp/if you want to resume - After a successful scan, vbsec cleans up
.vbsec-tmp/itself - Add
.vbsec-tmp/to your repo's.gitignore:.vbsec-tmp/
vbsec is a Claude Code skill — runs inside the agent CLI. Two integration shapes:
.git/hooks/pre-commit:
#!/usr/bin/env bash
# Requires Claude Code installed + vbsec plugin installed (/plugin install vbsec@vbsec)
set -e
REPORT=$(mktemp)
claude --no-stream -p '/vbs-scan-security staged lang=en' > "$REPORT"
VERDICT=$(awk '/^```json$/,/^```$/' "$REPORT" \
| sed '1d;$d' | jq -r '.verdict' 2>/dev/null || echo "")
case "$VERDICT" in
FAIL)
echo "vbsec FAIL — see findings:"
cat "$REPORT"
exit 1
;;
WARN)
echo "vbsec WARN — HIGH issues found, commit allowed but please fix soon."
;;
PASS)
echo "vbsec PASS"
;;
*)
echo "vbsec: could not parse verdict, allowing commit"
;;
esacNote: The hook only blocks on verdict=FAIL (CRITICAL present). WARN is informational.
.github/workflows/vbsec.yml:
name: vbsec security scan
on:
pull_request:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Claude Code
run: |
curl -fsSL https://claude.ai/install.sh | bash
echo "$HOME/.claude/bin" >> $GITHUB_PATH
- name: Install vbsec plugin
run: |
# Inside the Claude Code session: install vbsec marketplace + plugin
# (Run these via `claude --no-stream -p` if the CI image supports it,
# or use claude-code's plugin pre-install env vars.)
mkdir -p ~/.claude/plugins
# Option 1: pre-warm by cloning the plugin into the cache
git clone https://github.com/tanviet12/vbsec.git ~/.claude/plugins/cache/vbsec
# Option 2 (recommended once Claude Code CI tooling matures):
# claude plugin marketplace add tanviet12/vbsec
# claude plugin install vbsec@vbsec
- name: Run vbsec on PR
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
claude --no-stream -p "/vbs-scan-security pr id ${{ github.event.pull_request.number }} lang=en" \
> vbsec-report.md
- name: Post comment on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
path: vbsec-report.md
- name: Fail on CRITICAL
run: |
VERDICT=$(awk '/^```json$/,/^```$/' vbsec-report.md \
| sed '1d;$d' | jq -r '.verdict')
[ "$VERDICT" = "FAIL" ] && exit 1 || trueParse the JSON summary and apply your own policy:
# Block when ≥3 HIGH issues exist
HIGH_COUNT=$(jq -r '.counts.high' summary.json)
if [ "$HIGH_COUNT" -ge 3 ]; then
echo "Too many HIGH issues ($HIGH_COUNT). Blocking."
exit 1
fi
# Or: block on specific rules only
CRITICAL_RULES=$(jq -r '.findings[] | select(.severity=="CRITICAL") | .rule' summary.json)
if echo "$CRITICAL_RULES" | grep -q "HARDCODED-SECRET"; then
echo "Hardcoded secret detected — blocking deploy."
exit 1
fi- Read rules.md for full details on the 21 rules
- Want to add a new rule or language? See contributing.md