Scan your source code, your live site, or both. Finds bugs before your users do.
Get started · What it finds · Commands · Stacks · Demo videos
You ship a feature. A user finds the bug before you do. Sniff is the opposite of that.
Sniff is a tiny CLI that reads your source, opens your app in a headless browser, and hunts down bugs across eight dimensions — functional, visual, accessibility, performance, dead links, API endpoints, broken imports, and optional AI-assisted exploration. No API key required. No manual Playwright setup. No config.
npx sniff-qa
That's the whole setup. The rest of this README is detail.
npx sniff-qa auto-detects your framework, your dev server, your test scenarios. If npm run dev is running, sniff finds it.sniff --ci emits JUnit, tracks flakes, and fails on severity thresholds you set. sniff ci generates the GitHub Actions workflow./sniff-fix generates the patch.cd ~/projects/my-app # go to your project
npx sniff-qa # that's it
Sniff auto-detects everything: your framework, your dev server port, and your running app. If npm run dev is running, sniff finds it and runs browser checks too. No flags needed.
You can also be explicit:
npx sniff-qa --url http://localhost:3000 # specific local URL
npx sniff-qa --url https://myapp.com # production URL
No API keys. No manual Playwright install. No config files. Everything works out of the box. Browser checks auto-install the configured Playwright browser projects on first CLI run.
Sniff ships as an MCP server. Add it to your editor, then ask your AI to scan.
claude plugin marketplace add Aboudjem/10x
claude plugin install sniff@10x
This installs sniff as a Claude Code plugin from the 10x marketplace. Skills, commands, and the MCP server are wired up automatically.
claude mcp add sniff-qa npx sniff-qa --mcp
Add to ~/.cursor/mcp.json:
{ "mcpServers": { "sniff-qa": { "type": "stdio", "command": "npx", "args": ["sniff-qa", "--mcp"] } } }
Add to .vscode/mcp.json:
{ "servers": { "sniff-qa": { "type": "stdio", "command": "npx", "args": ["-y", "sniff-qa", "--mcp"] } } }
codex mcp add sniff-qa -- npx -y sniff-qa --mcp
Add to ~/.codeium/windsurf/mcp_config.json:
{ "mcpServers": { "sniff-qa": { "command": "npx", "args": ["sniff-qa", "--mcp"] } } }
Add to ~/.gemini/mcp_config.json:
{ "mcpServers": { "sniff-qa": { "command": "npx", "args": ["sniff-qa", "--mcp"] } } }
Add to .continue/mcpServers/sniff-qa.yaml:
mcpServers:
sniff-qa: { command: npx, args: [sniff-qa, --mcp], type: stdio }
clawhub install sniff-qa
Then ask: "Scan this project for issues" or "Check accessibility on localhost:3000" or "Discover and run E2E tests"
MCP tools: sniff({ mode: "scan" | "run" | "discover" | "report" }) — unified entry point. Plus sniff_install when Playwright browser binaries are missing. MCP returns a structured needsSetup payload instead of doing a long install inside stdio; call sniff_install or run the shown npx playwright install ... command, then retry. The legacy per-mode tools (sniff_scan, sniff_run, sniff_discover, sniff_report) remain available but are deprecated in v0.5 and will be removed in v0.7.
npm install -D sniff-qa
{
"scripts": {
"qa": "sniff"
}
}
That's all you need. Sniff auto-detects your dev server when it's running.
Requires Node.js 22+. Playwright installs automatically on first CLI browser scan; MCP asks you to run sniff_install when setup is missing.
npx sniff-qa # scan source + auto-detect dev server
npx sniff-qa --url https://myapp.com # scan source + specific URL
npx sniff-qa ./path/to/project # scan a specific directory
npx sniff-qa --ci # CI mode (JUnit output, no AI explorer)
npx sniff-qa --url http://localhost:3000 --explore # opt-in AI exploration
npx sniff-qa --verbose # show classifier top 3 + matched signals
npx sniff-qa discover --dry-run # preview generated scenarios, no browser
npx sniff-qa discover --force-app-type saas # skip classification, treat as SaaS
Sniff auto-detects your dev server by reading package.json scripts plus vite.config, nuxt.config, astro.config, and angular.json, then probing the framework's default port and the next 20 after it (to catch auto-increment when the default is taken). If a port responds with framework markers (Next.js, Vite, Nuxt, Astro, Remix, SvelteKit, Angular), browser checks run automatically. Ghost-process ports like macOS AirPlay on :5000 no longer collide.
Source checks run on every scan. Browser checks run automatically when sniff detects a running dev server, or when you pass --url.
| Command | What it does |
|---|---|
/sniff |
Scan the project. Auto-detects dev server, runs source + browser checks |
/sniff-fix |
Scan and auto-fix safe issues (debugger, console.log, etc.) |
/sniff-report |
Show results from the last scan |
sniff Scan source + auto-detect dev server
sniff --url <url> Scan source + test specific URL
sniff --url <url> --ci Full audit for CI pipelines
sniff <path> Scan a specific directory
sniff discover Autonomous E2E discovery (scenarios + edge cases)
sniff discover --regenerate-only Write sniff-scenarios/ tree without running
sniff fix Auto-fix safe issues (debugger, console.log)
sniff fix --check Dry run: show what would be fixed
sniff init Create sniff.config.ts
sniff ci Generate GitHub Actions workflow
sniff report Show last scan results
sniff update-baselines Accept current visual baselines
sniff doctor Check your environment (Node, Playwright, config, dev server)
sniff --help Show all commands and flags
sniff --version Show version
| Flag | What it does |
|---|---|
--url <url> |
Enable browser checks (accessibility, visual, performance, AI) |
--ci |
CI mode: skip AI explorer, add JUnit output, track flaky tests |
--explore |
Opt into AI-assisted exploration after browser checks |
--no-explore |
Compatibility flag; exploration is off by default |
--no-browser |
Source only even if --url is set |
--max-steps <n> |
Limit AI explorer steps (default: 50) |
--no-headless |
Show the browser window |
--headed |
Alias for --no-headless |
--format html,json,junit |
Choose report formats |
--fail-on critical,high |
Severities that cause non-zero exit |
--track-flakes |
Track test flakiness across runs |
--json |
JSON output for scripts |
--verbose |
Print classifier top-3 guesses and matched signals per dimension |
--dry-run |
Discover: generate scenarios without launching browser or writing reports |
--force-app-type <type> |
Discover: bypass the classifier (e.g. saas, ecommerce, booking) |
--app-type <types> |
Discover: filter classifier guesses to these app types (comma-separated) |
Sniff auto-detects your framework. No config needed.
| React JSX / TSX |
Next.js App + Pages |
Vue SFC |
Svelte Components |
Angular Templates |
Express Routes |
Vanilla HTML / CSS |
API discovery also supports Fastify, Hono, tRPC, and GraphQL.
Sniff works with zero config. Only create a config file if you want to customize.
npx sniff-qa init # auto-detects: .ts if TypeScript, .mjs if ESM, .js otherwise
npx sniff-qa init --ts # force TypeScript config
npx sniff-qa init --js # force JavaScript config (respects package.json "type")
import { defineConfig } from 'sniff-qa';
export default defineConfig({
// Save your URL so you can just run `sniff`
browser: {
baseUrl: 'http://localhost:3000',
projects: ['chromium'], // also supports 'firefox' and 'webkit'
},
// Viewports to test
viewports: [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'desktop', width: 1280, height: 720 },
],
// Toggle scanner groups
scanners: ['source', 'repo-analyzer', 'e2e', 'accessibility', 'visual', 'performance'],
accessibility: { enabled: true, standard: 'wcag21aa' },
visual: { enabled: true, threshold: 0.1 },
performance: { enabled: true, budgets: { lcp: 2500, fcp: 1800, tti: 3800 } },
// Optional AI providers. Default is deterministic local QA with provider: 'none'.
ai: {
provider: 'none', // codex-cli, claude-code, anthropic-api, openai-api, gemini-cli, ollama
model: undefined,
command: undefined,
baseUrl: undefined,
},
// Optional AI explorer, run only with --explore
exploration: { enabled: true, maxSteps: 50 },
// Dead link checker
deadLinks: {
scanCode: false, // docs/HTML by default; opt into JSX/TSX/JS/TS to reduce noise
checkExternal: true,
timeout: 5000,
retries: 2,
ignorePatterns: [],
maxConcurrent: 10,
},
// API endpoint discovery
apiEndpoints: {
checkErrorHandling: true,
checkValidation: true,
checkAuth: true,
checkSecrets: true,
frameworks: [], // empty = auto-detect all
},
// Turn off specific rules
rules: {
'debug-console-log': 'off',
},
});
Provider selection can also come from environment: SNIFF_AI_PROVIDER, SNIFF_AI_MODEL, SNIFF_AI_COMMAND, and SNIFF_AI_BASE_URL. API-backed providers use their normal keys: ANTHROPIC_API_KEY or OPENAI_API_KEY. If the selected provider is missing, Sniff skips AI generation and keeps the deterministic QA run moving.
| Rule | Severity | What it checks |
|---|---|---|
debug-console-log |
medium | console.log/debug/info |
debug-debugger |
high | debugger statements |
placeholder-lorem |
high | Lorem ipsum text |
placeholder-todo |
medium | TODO comments |
placeholder-fixme |
high | FIXME comments |
placeholder-tbd |
medium | TBD markers |
hardcoded-localhost |
medium | localhost URLs |
hardcoded-127 |
medium | 127.0.0.1 URLs |
broken-import |
medium | Unresolved imports |
dead-link-internal |
high | Broken file links |
dead-link-external |
medium | 404 external URLs |
dead-link-anchor |
medium | Missing anchors |
api-no-error-handling |
medium | Routes without try/catch |
api-no-validation |
medium | POST/PUT without validation |
api-no-auth |
low | Routes without auth |
api-hardcoded-secret |
critical | Hardcoded API keys |
Set any to 'off' to disable.
Run Sniff directly in CI:
npx sniff-qa --ci --format html,json,junit
Generate a GitHub Actions workflow:
npx sniff-qa ci
This writes .github/workflows/sniff.yml with Playwright caching, JUnit output, and report artifacts.
Flakiness quarantine: Tests that fail 3 of 5 runs get quarantined. They still run, still report, but won't block your pipeline.
sniff discover # run against the detected dev server
sniff discover --regenerate-only # write sniff-scenarios/ and exit (no run)
sniff discover reads your source (Prisma, Drizzle, TypeORM, Zod, GraphQL, OpenAPI, TS types), classifies what the app is (ecommerce, booking, saas, social, content, crm, auth-only, marketing, admin) with route tokens in English, French, Spanish, German, Portuguese, and Italian (a French SaaS on /fr/tableau-de-bord classifies correctly), generates happy-path journeys with real personas, enumerates edge variants (invalid email, XSS, payment declined, empty cart, offline, and more), and drives everything through Playwright.
Pass --verbose to see the top-3 candidates and every matched signal — useful for understanding why a classification happened. Pass --force-app-type <type> to skip classification entirely. Pass --dry-run to preview scenarios without launching a browser or writing reports.
Multilingual projects supported. The classifier matches French, Spanish, German, Portuguese (BR/PT), and Italian equivalents for every signature token, so a French SaaS on /fr/tableau-de-bord classifies the same as an English one on /dashboard.
What you get:
sniff-scenarios/_generated/<app-type>/<journey>.<variant>.scenario.md — reviewable scenarios with JSON frontmatter. Track them in git. Hand-edit them — sniff detects the change and asks before regenerating.sniff-reports/discovery/ — HTML, JSON, and JUnit reports per run.Flags:
| Flag | What it does |
|---|---|
--url <url> |
Target URL (default: auto-detect dev server) |
--max-scenarios <n> |
Cap total scenarios (default: 50) |
--max-variants-per-scenario <n> |
Cap edge variants per scenario (default: 3) |
--max-variants-per-run <n> |
Cap edge variants per run (default: 40) |
--realism <profile> |
robot, careful-user, casual-user (default), frustrated-user, power-user |
--seed <n> |
Replay a specific random seed |
--only <filter> |
Filter scenarios by id substring or app type |
--app-type <types> |
Filter classifier guesses to these app types (comma-separated, kept for back-compat) |
--force-app-type <type> |
Bypass the classifier entirely — treat the app as this type |
--verbose |
Print classifier breakdown (top 3 guesses + matched signals per dimension) |
--dry-run |
Generate scenarios without launching the browser or writing reports |
--regenerate |
Regenerate sniff-scenarios/ before running |
--regenerate-only |
Regenerate and exit (no browser) |
--force-regenerate |
Overwrite hand-edits without prompting |
--non-interactive |
Skip prompts and the production-URL countdown |
--verbose |
Print classification breakdown (top 3 guesses + matched signals per dimension) |
--no-llm |
Skip Claude Code CLI polish (deterministic only) |
Production-URL safety: when the target is not a local address, sniff prints a 5-second countdown banner before running. Hit Ctrl+C to cancel. No --allow-destructive flag.
LLM polish (optional): discovery can use the claude CLI to break close-call app-type classifications. Responses are cached under .sniff/discover/cache/. Pass --no-llm to skip.
No telemetry. No signup. No data collection. No API keys required. Your code stays on your machine unless you explicitly configure an API-backed AI provider.
[!NOTE] All deterministic checks work without any API key. Optional AI providers are off by default and must be selected through config or environment. Dead link checking validates external URLs but never sends your code.
Add a source rule: each rule is a regex + severity in src/scanners/source/rules/. See CONTRIBUTING.md.
Built on Playwright · axe-core · Lighthouse · pixelmatch · Zod · MCP SDK · Anthropic SDK
Built by Adam Boudjemaa · Apache 2.0