scan_changed_files
Scan only files modified since a specified git ref. Returns findings for changed files, ideal for PR checks and incremental CI.
Instructions
Scan only files that have changed since a given git ref (branch, commit, or HEAD~N). Ideal for PR checks, pre-push hooks, and incremental CI. Returns findings only for modified/added files.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | Repository root path | . |
| base | No | Git ref to diff against (e.g. 'main', 'HEAD~3', commit SHA) | HEAD~1 |
| format | No | Output format | markdown |
Implementation Reference
- src/index.ts:660-744 (handler)MCP tool registration and handler for 'scan_changed_files'. Takes path, base (git ref), and format params. Runs git diff --name-only --diff-filter=ACMR against the base ref, scans each changed file using analyzeCode, and returns findings in markdown or JSON format. Records scan stats.
// Tool 24: Scan changed files only — for incremental CI/CD and PR workflows server.tool( "scan_changed_files", "Scan only files that have changed since a given git ref (branch, commit, or HEAD~N). Ideal for PR checks, pre-push hooks, and incremental CI. Returns findings only for modified/added files.", { path: z.string().default(".").describe("Repository root path"), base: z.string().default("HEAD~1").describe("Git ref to diff against (e.g. 'main', 'HEAD~3', commit SHA)"), format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), }, async ({ path: repoPath, base, format }) => { const { execFileSync } = await import("child_process"); const { readFileSync, existsSync } = await import("fs"); const { resolve, extname, basename } = await import("path"); const { EXTENSION_MAP, CONFIG_FILE_MAP } = await import("./utils/constants.js"); const root = resolve(repoPath); let changedFiles: string[]; try { const output = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", base], { cwd: root, encoding: "utf-8" }); changedFiles = output.trim().split("\n").filter(Boolean); } catch { return { content: [{ type: "text", text: format === "json" ? JSON.stringify({ error: "Failed to get git diff" }) : "Error: Failed to get git diff. Ensure you're in a git repository." }] }; } if (changedFiles.length === 0) { const empty = format === "json" ? JSON.stringify({ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0, blocked: false }, findings: [] }) : "No changed files to scan."; return { content: [{ type: "text", text: empty }] }; } const rules = getRules(); const allFindings: Array<{ file: string; id: string; name: string; severity: string; owasp: string; line: number; match: string; fix: string; fixCode?: string }> = []; for (const relPath of changedFiles) { const fullPath = resolve(root, relPath); if (!existsSync(fullPath)) continue; const ext = extname(relPath).toLowerCase(); let language = EXTENSION_MAP[ext]; if (!language && basename(relPath).startsWith("Dockerfile")) language = "dockerfile"; if (!language) language = CONFIG_FILE_MAP[basename(relPath)]; if (!language) continue; try { const content = readFileSync(fullPath, "utf-8"); const findings = analyzeCode(content, language, undefined, fullPath, root, rules); for (const f of findings) { allFindings.push({ file: relPath, id: f.rule.id, name: f.rule.name, severity: f.rule.severity, owasp: f.rule.owasp, line: f.line, match: f.match, fix: f.rule.fix, fixCode: f.rule.fixCode, }); } } catch { /* skip unreadable files */ } } // Record stats recordScan(root, { toolName: "scan_changed_files", filesScanned: changedFiles.length, findings: allFindings.map(f => ({ severity: f.severity, ruleId: f.id })) }); const statsSummary = getSummaryLine(root, allFindings.length, format); if (format === "json") { const critical = allFindings.filter(f => f.severity === "critical").length; const high = allFindings.filter(f => f.severity === "high").length; const medium = allFindings.filter(f => f.severity === "medium").length; return { content: [{ type: "text", text: mergeStatsIntoOutput(JSON.stringify({ summary: { total: allFindings.length, critical, high, medium, low: 0, blocked: critical > 0 || high > 0, changedFiles: changedFiles.length }, findings: allFindings, }), statsSummary, format) }] }; } // Markdown const lines = [`# GuardVibe Changed Files Report`, ``, `Base: ${base}`, `Changed files: ${changedFiles.length}`, `Issues found: ${allFindings.length}`, ``]; if (allFindings.length === 0) { lines.push(`All changed files passed security checks.`); } else { const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }; allFindings.sort((a, b) => (severityOrder[a.severity] ?? 99) - (severityOrder[b.severity] ?? 99)); for (const f of allFindings) { lines.push(`- [${f.severity.toUpperCase()}] **${f.name}** (${f.id}) in \`${f.file}\`:${f.line} — ${f.fix}`); } } return { content: [{ type: "text", text: mergeStatsIntoOutput(lines.join("\n"), statsSummary, format) }] }; } ); - src/index.ts:665-668 (schema)Zod schema for scan_changed_files input: path (default '.'), base (default 'HEAD~1'), format (markdown or json).
path: z.string().default(".").describe("Repository root path"), base: z.string().default("HEAD~1").describe("Git ref to diff against (e.g. 'main', 'HEAD~3', commit SHA)"), format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), }, - src/index.ts:660-662 (registration)Registration of the scan_changed_files MCP tool via server.tool() call.
// Tool 24: Scan changed files only — for incremental CI/CD and PR workflows server.tool( "scan_changed_files", - src/index.ts:7-144 (helper)Imports used by the scan_changed_files handler: analyzeCode for security analysis, recordScan/getSummaryLine for stats, and mergeStatsIntoOutput for formatting.
import { checkCode, analyzeCode } from "./tools/check-code.js"; const require = createRequire(import.meta.url); const pkg = require("../package.json") as { version: string }; import { checkProject } from "./tools/check-project.js"; import { getSecurityDocs } from "./tools/get-security-docs.js"; import { checkDependencies } from "./tools/check-deps.js"; import { scanDirectory } from "./tools/scan-directory.js"; import { scanDependencies } from "./tools/scan-dependencies.js"; import { scanSecrets } from "./tools/scan-secrets.js"; import { scanStaged } from "./tools/scan-staged.js"; import { complianceReport } from "./tools/compliance-report.js"; import { exportSarif } from "./tools/export-sarif.js"; import { checkPackageHealth } from "./tools/check-package-health.js"; import { fixCode } from "./tools/fix-code.js"; import { auditConfig } from "./tools/audit-config.js"; import { generatePolicy } from "./tools/generate-policy.js"; import { reviewPr } from "./tools/review-pr.js"; import { scanSecretsHistory } from "./tools/scan-secrets-history.js"; import { policyCheck } from "./tools/policy-check.js"; import { analyzeTaint, formatTaintFindings } from "./tools/taint-analysis.js"; import { analyzeCrossFileTaint, formatCrossFileTaintFindings } from "./tools/cross-file-taint.js"; import { checkCommand } from "./tools/check-command.js"; import { scanConfigChange } from "./tools/scan-config-change.js"; import { repoSecurityPosture } from "./tools/repo-posture.js"; import { explainRemediation } from "./tools/explain-remediation.js"; import { discoverPlugins } from "./plugins/loader.js"; import { builtinRules } from "./data/rules/index.js"; import type { SecurityRule } from "./data/rules/types.js"; import { loadConfig } from "./utils/config.js"; import { setRules, getRules } from "./utils/rule-registry.js"; import { recordScan, recordFix, recordSecrets, recordDependencyCVEs, recordGrade, getSummaryLine } from "./lib/stats.js"; import { securityStats } from "./tools/security-stats.js"; import { auditMcpConfig } from "./tools/audit-mcp-config.js"; import { scanHostConfig } from "./tools/scan-host-config.js"; import { doctor } from "./tools/doctor.js"; import { formatHostFindings, redactSecrets } from "./server/types.js"; import { verifyFix } from "./tools/verify-fix.js"; import { fixCode as fixCodeTool, type FixSuggestion } from "./tools/fix-code.js"; import { analyzeAuthCoverage, formatAuthCoverage } from "./tools/auth-coverage.js"; import { buildDeepScanPrompt, parseDeepScanResult, formatDeepScanFindings, callLLM } from "./tools/deep-scan.js"; import { runFullAudit, formatAuditResult } from "./tools/full-audit.js"; import { generateRemediationPlan, formatRemediationPlan } from "./tools/remediation-plan.js"; // verify-remediation logic is inline in the tool handler below // Helper: merge stats summary into JSON output instead of concatenating two JSON objects function mergeStatsIntoOutput(results: string, summary: string, format: string): string { if (format === "json" && summary) { try { const parsed = JSON.parse(results); const stats = JSON.parse(summary); return JSON.stringify({ ...parsed, _meta: stats.guardvibeStats ?? stats }); } catch { /* fall through */ } } return results + summary; } const server = new McpServer({ name: "guardvibe", version: pkg.version, description: "Security MCP for vibe coding — single source of truth for AI assistants. 390 security rules and 36 tools. Use full_audit for a comprehensive PASS/FAIL/WARN verdict with deterministic result hash, coverage %, and unified report across code, secrets, dependencies, config, taint analysis, and auth coverage. IMPORTANT: When full_audit returns FAIL/WARN, call remediation_plan to get a mandatory section-by-section fix checklist covering ALL 6 sections (not just code). After fixing, call verify_remediation to confirm all sections were addressed. Same code = same hash = same results regardless of which AI assistant runs it. Covers OWASP, Next.js, Supabase, Stripe, Clerk, Prisma, Hono, AI SDK, MCP server security, host hardening. Maps to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EU AI Act. Runs 100% locally with zero configuration.", }); // Tool 1: Analyze code for security vulnerabilities server.tool( "check_code", "Analyze inline code for security vulnerabilities (OWASP Top 10, XSS, SQL injection, insecure patterns). Pass code as a string parameter. For scanning files on disk, use scan_file instead. Example: check_code({code: 'app.get(...)', language: 'javascript'})", { code: z.string().describe("The code snippet to analyze"), language: z .enum(["javascript", "typescript", "python", "go", "dockerfile", "html", "sql", "shell", "yaml", "terraform", "firestore"]) .describe("Programming language of the code"), framework: z .string() .optional() .describe("Framework context (e.g. express, nextjs, fastapi, react, django)"), format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"), }, async ({ code, language, framework, format }) => { const rules = getRules(); const results = checkCode(code, language, framework, undefined, undefined, format, rules); const findings = analyzeCode(code, language, framework, undefined, undefined, rules); const cwd = process.cwd(); recordScan(cwd, { toolName: "check_code", filesScanned: 1, findings: findings.map(f => ({ severity: f.rule.severity, ruleId: f.rule.id })) }); const summary = getSummaryLine(cwd, findings.length, format); return { content: [{ type: "text", text: mergeStatsIntoOutput(results, summary, format) }], }; } ); // Tool 2: Scan entire project for security vulnerabilities server.tool( "check_project", "Scan multiple files for security vulnerabilities and generate a project-wide security report with a security score. Use this for comprehensive security audits.", { files: z .array( z.object({ path: z.string().describe("Relative file path (e.g. src/app.ts)"), content: z.string().describe("File source code"), }) ) .describe("List of files to scan: [{path, content}]"), format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"), }, async ({ files, format }) => { const rules = getRules(); const results = checkProject(files, format, rules); let findingCount = 0; const cwd = process.cwd(); try { const parsed = JSON.parse(results); findingCount = parsed?.summary?.total ?? 0; const grade = parsed?.summary?.grade; const score = parsed?.summary?.score; if (grade && score != null) recordGrade(cwd, grade, score); recordScan(cwd, { toolName: "check_project", filesScanned: files.length, findings: (parsed?.findings ?? []).map((f: any) => ({ severity: f.severity, ruleId: f.id })) }); } catch { const m = /Issues found:\s*(\d+)/.exec(results); findingCount = m ? parseInt(m[1], 10) : 0; recordScan(cwd, { toolName: "check_project", filesScanned: files.length, findings: [] }); } const summary = getSummaryLine(cwd, findingCount, format); return { content: [{ type: "text", text: mergeStatsIntoOutput(results, summary, format) }], }; } ); // Tool 3: Get security documentation and best practices (renumbered from Tool 2) server.tool( "get_security_docs", "Get security best practices and remediation guidance for a specific topic, framework, or vulnerability type. Covers OWASP Top 10, framework-specific hardening (Next.js, Supabase, Stripe), and secure coding patterns. Returns actionable guidance with code examples.", { topic: z .string() .describe( - src/index.ts:718-718 (helper)Stats recording and summary generation for scan_changed_files results.
recordScan(root, { toolName: "scan_changed_files", filesScanned: changedFiles.length, findings: allFindings.map(f => ({ severity: f.severity, ruleId: f.id })) });