
Oma Deepsec
- 18 installs
- 41 repo stars
- Updated August 4, 2026
- gracefullight/stock-checker
Drive Vercel's deepsec agent-powered vulnerability scanner end-to-end with cost-aware scan, triage, and PR gating.
About
Operates Vercel's deepsec scanner in a repo: bootstraps the .deepsec workspace, runs scan/process/triage/revalidate/export passes, and gates PRs via process --diff. A developer uses it to run cost-conscious agentic security scans and CI-based PR security review.
- Cost-aware scan/process/triage/revalidate/export sequence
- PR/CI security gating with process --diff and custom matchers
Oma Deepsec by the numbers
- 18 all-time installs (skills.sh)
- Ranked #1,596 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gracefullight/stock-checker --skill oma-deepsecAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 41 |
| Last updated | August 4, 2026 |
| Repository | gracefullight/stock-checker ↗ |
What it does
Drive Vercel's deepsec agent-powered vulnerability scanner end-to-end with cost-aware scan, triage, and PR gating.
Files
Deepsec: Agent-Powered Vulnerability Scanner Driver
Scheduling
Goal
Operate Vercel's deepsec security scanner inside a target repository safely and cost-consciously: bootstrap the .deepsec/ workspace, write a tight INFO.md, run the right scan/process/triage/revalidate/export sequence, gate PRs in CI via process --diff, and grow project-specific matchers, surfacing real, revalidated findings without runaway spend.
Intent signature
- User mentions
deepsec, "deep security scan",bunx deepsec,pnpm deepsec,npx deepsec. - User asks an agent to scan a repository for vulnerabilities, security issues, or CVEs and the project has (or should have) a
.deepsec/directory. - User asks how to add a deepsec PR / CI security gate, or about
process --diff,--diff-staged,--diff-working,--files-from,--comment-out. - User mentions deepsec artefacts:
INFO.md,SETUP.md,data/<id>/files/,FileRecord,RunMeta,revalidation,triage, custom matchers,MatcherPlugin,noiseTier,priorityPaths. - User asks about deepsec configuration:
deepsec.config.ts,defaultAgent,AI_GATEWAY_API_KEY,VERCEL_OIDC_TOKEN, AI Gateway, Vercel Sandbox,--agent codex,--agent claude. - User asks how to lower deepsec cost, cut false-positive rate, or interpret severity / triage / revalidation verdicts.
When to use
- First-time deepsec install in a repo (
init,INFO.mdwrite, first calibration scan). - Running a full or scoped scan and processing findings.
- Setting up a per-PR CI gate with
process --diffand--comment-out. - Writing a project-specific matcher to cover entry points the default set misses.
- Triaging a backlog of findings (severity bucketing, FP cuts via
revalidate, exporting to issue tracker). - Diagnosing deepsec failures: missing credentials, AI Gateway quota stops, refusals, sandbox auth.
When NOT to use
- Generic OWASP / lint-style review without deepsec → use
oma-qa. - Generic CVE / dependency advisories → use
oma-qaoroma-search. - Architecting a brand-new SAST pipeline that is not deepsec → use
oma-architecture. - Writing or auditing application code itself → route to
oma-backend/oma-frontend/oma-mobile. - Cloud / IAM / Terraform hardening → use
oma-tf-infra(deepsec only scans the IaC; remediation lives there). - Pure reasoning about a finding's fix in product code → use
oma-debugonce deepsec has produced the finding.
Expected inputs
target_repo_root: absolute path of the codebase to scan (parent of.deepsec/).intent: one ofsetup|scan|pr-review|matchers|triage|config|troubleshoot.credential_mode:ai-gateway-key|vercel-oidc|direct-anthropic|direct-openai|subscription.agent_choice:codex(upstream default; modelgpt-5.5) orclaude(modelclaude-opus-4-8). Asked once before the first paid call if not already provided.severity_floor: lowest severity worth surfacing (typicallyHIGH).- Optional: existing
.deepsec/data/<id>/,deepsec.config.ts, custom matchers, CI provider.
Expected outputs
- A working
.deepsec/workspace registered against the target repo. - A populated
data/<id>/INFO.md(50-100 lines, project-specific, no line numbers). - One or more completed
scan→process(→triage/revalidate) runs with reproducible cost notes. - For PR mode: a CI workflow file using
process --diff <base>with two-job split (no PR-write in PR-code job). - For matchers: new
.deepsec/matchers/<slug>.tsfiles wired through the inline plugin indeepsec.config.ts. - A findings export (
md-dirand/orjson) plus a short summary of top severities and FP-rate notes. - Explicit, dollar-and-time-bounded plan before any pass that may cost more than ~$25.
Dependencies
- Node.js 22+, plus a package manager:
bun/bunx(preferred in this monorepo),pnpm,npm, oryarn. - A working AI credential:
AI_GATEWAY_API_KEY=vck_…, orVERCEL_OIDC_TOKEN, or directANTHROPIC_AUTH_TOKEN+ANTHROPIC_BASE_URL, or a logged-inclaude/codexCLI subscription. - Git (history is consulted by
revalidateand--diffmodes). - Optional: Vercel Sandbox auth for
deepsec sandbox …distributed runs. - Reference resources under
resources/(loaded only when the scenario requires them).
Control-flow features
- Branches by
intent(setup vs scan vs pr-review vs matchers vs triage vs config vs troubleshoot). - Branches by repo size (calibrate with
--limit 50before any large pass). - Branches by credential source (gateway key, OIDC, direct, subscription).
- Stops on quota / credit exhaustion and resumes the same command after top-up.
- Refuses to launch an unbounded
processwhen no calibration has been done and the repo is large. - Reads codebase, writes
.deepsec/files and CI configs, runs long-lived AI processes.
Structural Flow
Entry
1. Confirm whether .deepsec/ already exists; if yes, treat the run as incremental, never re-init. 2. Resolve intent from the user prompt; if ambiguous (e.g. "scan this repo"), default to setup then scan (calibration mode). 3. Estimate scale: count source files (rough rg --files | wc -l excluding node_modules, .git, dist) to forecast cost before any AI pass. 4. Check for an AI credential in .env.local or shell env; if none, route to credential setup before any process / revalidate / triage call. 5. Confirm agent choice with the user before the first paid call. If agent_choice is not already in the prompt and deepsec.config.ts does not pin a defaultAgent, ask whether to run codex (gpt-5.5, the upstream default; runs in a strict sandbox, cheaper, grep-heavy) or claude (claude-opus-4-8; strongest reasoning, most expensive). The two backends can be mixed via --reinvestigate and findings dedupe across agents. Skip the question if the user has already named an agent or has explicitly delegated the decision ("just pick reasonable defaults").
Scenes
1. PREPARE: Resolve intent, repo root, credential, budget cap, severity floor, agent choice. Refuse to run blind on a repo of unknown scale. 2. ACQUIRE: Read .deepsec/deepsec.config.ts, data/<id>/project.json, INFO.md, last runs/ entries, and target-repo signals (README, AGENTS.md/CLAUDE.md, framework configs, route directories) needed to author or verify INFO.md. 3. REASON: Pick the smallest pass that answers the user's question. Options include scan only, a --limit 50 calibration, a full process, process --diff, a matcher-authoring loop, or troubleshoot-only. Always state cost forecast and stopping condition before AI passes. 4. ACT: Run the planned commands from inside .deepsec/. For matchers, write per-slug files and wire the inline plugin. For PR mode, scaffold the two-job CI workflow. 5. VERIFY: Use deepsec status, the run's RunMeta, exit code (0 clean, 1 findings produced, other = error), candidate counts, and (when present) the --comment-out markdown to confirm output. 6. FINALIZE: Summarize findings by severity and verdict, list dollar cost and wall time, name files written, and call out follow-ups (revalidate HIGH+, write matchers for missed entry points, persist data/ between CI runs).
Transitions
- If
.deepsec/is missing and intent involves scanning → runbunx deepsec init(ornpx deepsec init) and follow the printed prompt to populateINFO.mdbefore any AI pass. - If
INFO.mdis empty or template-shaped → write it (50-100 lines, project-specific, 3-5 examples per section, no line numbers, no generic CWE enumeration). - If repo is > 500 files and no calibration has run → run a calibration pass first (deepsec docs recommend
--limit 50 --concurrency 5) and report cost extrapolation before the full pass. - If a
process/revalidaterun halts on quota → leave file locks intact, surface the exact remediation URL, re-run the same command after top-up. - If the agent reports a refusal (
refused: true) → never silently drop; document the affected files and either retry with the other backend or add the path toconfig.json:ignorePathsonly if reproducible. - If the user wants a CI gate → emit the two-job pattern (PR-code job has no
pull-requests: write, comment job has no PR code). - If the user wants more matcher coverage → run the matcher-authoring workflow against
data/<id>/files/and the parent repo's entry points.
Failure and recovery
| Failure | Recovery |
|---|---|
Missing AI credentials for --agent claude / codex | Pick a credential mode (gateway key / OIDC / direct / subscription) per resources/config.md and write .env.local. |
401 Unauthorized from gateway | OIDC: re-run vercel env pull (12 h expiry). API key: regenerate. Confirm .env.local is in the cwd deepsec runs from. |
Stopped: AI Gateway credits exhausted | Top up via the printed URL; re-run the same command, files already done are skipped. |
Stopped: Claude Pro/Max subscription exhausted | Switch to AI Gateway; subscriptions don't carry full scans. |
| Persistent refusal on a single file (>5% of batches) | Add the path to data/<id>/config.json:ignorePaths, or run that file alone with --batch-size 1. |
FP rate too high on HIGH+ | Run revalidate --min-severity HIGH; tighten INFO.md's threat model and FP notes; bias matchers to precise. |
noisy matcher wedges scanner on a 100k-file repo | Tighten filePatterns to language- or directory-anchored globs. |
| Sandbox auth fails | OIDC: re-run vercel env pull. Access-token mode: verify VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID. |
| User asks for full scan with no budget context | Halt; report file count and forecast cost band; require explicit go-ahead before the full pass. |
Exit
- Success: planned passes ran, findings exist with verdicts (or no findings produced), files written are listed, residual cost / followups are explicit.
- Partial success: some passes blocked on credentials/quota/refusal; the blocker, the safe-resume command, and the recommended next step are reported.
- Failure: nothing destructive happened, the user has the exact next command to unblock the work.
Logical Operations
Actions
| Action | SSL primitive | Evidence |
|---|---|---|
| Detect existing workspace and credentials | READ | .deepsec/, .env.local, env vars |
| Estimate repo scale | INFER | `rg --files |
| Choose pass plan (calibrate vs full vs diff) | SELECT | File count, intent, budget cap |
| Init workspace | CALL_TOOL | bunx deepsec init |
Write INFO.md | WRITE | data/<id>/INFO.md |
| Run scan | CALL_TOOL | bunx deepsec scan |
| Run AI investigation | CALL_TOOL | bunx deepsec process (--limit, --concurrency) |
| Triage / revalidate | CALL_TOOL | bunx deepsec triage / revalidate --min-severity HIGH |
| Export findings | CALL_TOOL | `bunx deepsec export --format md-dir |
| PR-mode review | CALL_TOOL | bunx deepsec process --diff <base> --comment-out comment.md |
| Author custom matcher | WRITE | .deepsec/matchers/<slug>.ts + inline plugin in deepsec.config.ts |
| Validate matcher hit rate | VALIDATE | bunx deepsec scan --matchers <slug> candidate count |
| Verify and report | NOTIFY | RunMeta, severity counts, dollar cost, FP rate |
| Stop on budget breach | TERMINATE | Refuse unbounded process without calibration |
Tools and instruments
- Package manager:
bun/bunx(preferred),pnpm,npm,yarnare interchangeable. - CLI commands:
deepsec init,init-project,scan,process,process --diff,triage,revalidate,enrich,report,export,metrics,status,sandbox <cmd>. - Diff sources for PR mode:
--diff <ref|range>,--diff-staged,--diff-working,--files <csv>,--files-from <path>(or-for stdin). - Inspection:
jqoverdata/<id>/files/**/*.jsonfor ad-hoc severity / TP queries. - Credentials:
AI_GATEWAY_API_KEY,VERCEL_OIDC_TOKEN,ANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL,OPENAI_API_KEY/OPENAI_BASE_URL,claude login,codex login. - Resource files under
resources/for setup, scanning, PR review, matchers, triage, config, load on demand.
Canonical workflow path
1. Bootstrap (one time per repo):
cd <target-repo>
bunx deepsec init
cd .deepsec
bun install
# Edit .env.local: set AI_GATEWAY_API_KEY=vck_… (or VERCEL_OIDC_TOKEN via `vercel env pull`)Then prompt the coding agent (this skill) to read .deepsec/node_modules/deepsec/SKILL.md and .deepsec/data/<id>/SETUP.md, skim README / AGENTS.md / CLAUDE.md and a handful of representative files, and replace each section of data/<id>/INFO.md (50-100 lines, 3-5 examples per section, no line numbers, no generic CWE rehash). 2. Calibrate before any full pass. The deepsec docs (getting-started.md, vercel-setup.md, faq.md) recommend --limit 50 --concurrency 5 as the calibration starting point.
bunx deepsec scan
bunx deepsec status
bunx deepsec process --limit 50 --concurrency 5Read the per-batch cost. Extrapolate to full repo. Get the user's explicit go-ahead before the full process. If the user names different --limit / --concurrency values, use theirs. 3. Full investigation, triage, revalidate, export:
bunx deepsec process --concurrency 5
bunx deepsec triage --severity HIGH
bunx deepsec revalidate --min-severity HIGH
bunx deepsec export --format md-dir --out ./findings
bunx deepsec metrics4. PR mode (CI gate, scoped to changed files, exit code = 0/1):
bunx deepsec process \
--diff origin/${BASE_REF} \
--comment-out comment.mdWire the two-job CI pattern from resources/pr-review.md. Never grant pull-requests: write to the job that runs PR-controlled code. 5. Custom matchers (close entry-point gaps surfaced in step 3):
- Read the contract in
.deepsec/node_modules/deepsec/dist/config.d.tsand thesamples/webapp/matchers/*examples. - Write
.deepsec/matchers/<slug>.ts, wire it through the inline plugin in.deepsec/deepsec.config.ts. - Verify hit rate:
bunx deepsec scan --matchers <slug>should land in 1-20 hits / 1k files (precise), 5-100 (normal), or roughly the framework entry-point count (noisy).
6. Resume after any quota stop, network blip, or Ctrl-C: re-run the same command. State is on disk under .deepsec/data/<id>/.
Resource scope
| Scope | Resource target |
|---|---|
CODEBASE | Target repo source files, framework configs, route directories, README / AGENTS.md / CLAUDE.md. |
LOCAL_FS | .deepsec/deepsec.config.ts, .deepsec/.env.local, .deepsec/matchers/, .deepsec/data/<id>/{project.json,INFO.md,config.json,files/,runs/,reports/}, generated findings/, comment.md, CI workflow files. |
PROCESS | `bunx deepsec scan |
NETWORK | Anthropic / OpenAI via Vercel AI Gateway (default) or direct provider endpoints; optional Vercel Sandbox microVM control plane. |
CREDENTIALS | AI_GATEWAY_API_KEY, VERCEL_OIDC_TOKEN, ANTHROPIC_AUTH_TOKEN, OPENAI_API_KEY, VERCEL_TOKEN / VERCEL_TEAM_ID / VERCEL_PROJECT_ID, claude / codex subscription tokens. Consume read-only; never echo secrets back to the user or commit them. |
MEMORY | User-stated budget cap, severity floor, and stop conditions for the current session. |
Preconditions
- Node.js 22+ is available.
- Repo is a git checkout (deepsec uses git history for
revalidateand--diff). - For any AI command: at least one credential mode is configured before the call, or the call is held until one is.
- For
sandboxmode: Vercel auth is wired; otherwise stay local. - For unbounded
processruns on > 500-file repos: a--limitcalibration pass has produced a cost number the user has acknowledged.
Effects and side effects
- Creates
.deepsec/(config, lockfile, scaffolding) and.deepsec/data/<id>/(gitignored) inside the target repo. - Writes
.env.local(never commit) and may runvercel link/vercel env pull(writes.vercel/project.json+ token). - Spawns long-running AI processes that cost real money. Single full scans range from $25 to over $1,200 per the official cost guide and can climb to tens of thousands on very large repos.
- Reads source code; sends snippets to the configured LLM (gateway = zero retention; direct provider = subject to that provider's policy). Never exfiltrates secrets; the gateway key stays outside the worker sandbox in
sandboxmode. - May write
.github/workflows/deepsec.yml(or analogue) when the user asks for a CI gate. - Edits
deepsec.config.tsand adds.deepsec/matchers/*.tswhen authoring matchers. - Does not commit, push, or open PRs unless the user explicitly authorizes a separate commit step (route via
oma-scm).
Guardrails
1. Never launch an unbounded `process` on a repo whose size you have not measured. Always run a calibration pass first when file count is unknown or > 500 (deepsec docs recommend --limit 50 --concurrency 5; defer to a user-named value if given). 2. State cost and stopping condition before any AI pass. Use the published bands (100 files ≈ $25-60, 500 ≈ $130-300, 2,000 ≈ $500-1,200; ×2-3 swing). 3. Resume, do not reset. After any network / quota / Ctrl-C interruption, re-run the same command. Never delete data/<id>/ to "start clean" without explicit user instruction. 4. `INFO.md` stays short and project-specific. 50-100 lines, 3-5 examples per section. Name primitives but no line numbers. Skip generic CWE categories; built-in matchers cover those. 5. For PR/CI gates, keep PR-controlled code in a no-write job. Never grant pull-requests: write to a job that executes PR-controlled pnpm install / config-loading. Use the two-job pattern in resources/pr-review.md. 6. Pin actions to full SHAs in production CI; major-version tags are for examples only. 7. Never silently drop refusals. If the agent reports refused: true, log it, retry with the other backend, or add the file to ignorePaths only when reproducible. 8. Bias matchers toward `precise` when the bug shape is exact. Reserve noisy for entry-point coverage and tight globs. 9. Never echo or commit credentials (vck_…, sk-ant-…, sk-…, OIDC tokens). Treat .env.local as secret. Treat data/ as gitignored by default. 10. Treat deepsec like an agent with shell access. Recommend sandbox for prompt-injection-prone repos (vendored code, untrusted deps). 11. Findings need verdicts. For any HIGH+ surfaced to the user, prefer revalidate-tagged verdicts (true-positive / false-positive / fixed / uncertain) over raw process output. 12. Do not invent CLI flags. Anything beyond resources/scanning.md's flag list must be checked against --help first. 13. Ask agent choice before the first paid call. If the user has not named an agent (claude vs codex) and deepsec.config.ts does not pin defaultAgent, ask once with the trade-off clearly stated. Do not also bargain over budget or severity; those are handled via the upstream calibration recommendation (--limit 50 --concurrency 5 per deepsec docs) and the user-stated severity_floor.
References
- Workspace install +
INFO.mdbootstrap:resources/setup.md - Full scan/process/triage/revalidate/export workflow + cost guide:
resources/scanning.md - PR / CI gate via
process --diff(two-job pattern, exit-code semantics):resources/pr-review.md - Authoring custom matchers (slugs, noise tiers, file globs, plugin wiring):
resources/matchers.md - Reading findings, severities, triage / revalidation verdicts, FP cuts:
resources/triage.md deepsec.config.tsreference, env vars, plugin order, AI Gateway / Vercel Sandbox auth:resources/config.md- Upstream docs (load only when a resource file points at one):
- Repo + README: https://github.com/vercel-labs/deepsec
- Per-topic docs at https://github.com/vercel-labs/deepsec/tree/main/docs (
getting-started,reviewing-changes,writing-matchers,configuration,models,plugins,architecture,data-layout,vercel-setup,supported-tech,faq) - Shared context loading:
../_shared/core/context-loading.md - Shared quality principles:
../_shared/core/quality-principles.md
Configuration: deepsec.config.ts, env vars, plugins, models
deepsec reads deepsec.config.{ts,mjs,js,cjs} from the current working directory, walking up. The CLI inherits whatever the file declares.
import { defineConfig } from "deepsec/config";
import myPlugin from "@my-org/deepsec-plugin-foo";
export default defineConfig({
projects: [
{ id: "my-app", root: "../my-app" },
{ id: "service", root: "../service",
githubUrl: "https://github.com/me/service/blob/main" },
],
plugins: [myPlugin()],
});For a fully-worked example exercising every common field (infoMarkdown, promptAppend, priorityPaths, an inline plugin), see samples/webapp/deepsec.config.ts in the deepsec repo.
Top-level fields
| Field | Type | Purpose |
|---|---|---|
projects | ProjectDeclaration[] | Codebases deepsec knows about. |
plugins | DeepsecPlugin[] | Loaded in order; later plugins override single-slot capabilities. |
matchers | { only?: string[]; exclude?: string[] } | Filter the matcher set used by scan. |
defaultAgent | `"claude" | "codex"` |
dataDir | string | Override the data/ directory. Defaults to ./data. |
ProjectDeclaration
| Field | Type | Required | Purpose |
|---|---|---|---|
id | string | yes | Used as --project-id and the data directory name. |
root | string | yes | Absolute or relative path to the codebase. |
githubUrl | string | no | https://github.com/owner/repo/blob/branch for clickable links in exports. Auto-detected from git remote if omitted. |
infoMarkdown | string | no | Repo context injected into AI prompts. Overrides data/<id>/INFO.md if both are set. |
promptAppend | string | no | Free-form text appended to the system prompt for this project. |
priorityPaths | string[] | no | Path prefixes to process first. |
Per-project data/<id>/config.json
Optional, read by scan and the AI agents. Overrides the same fields on the project declaration if both are present.
{
"priorityPaths": ["app/api/", "lib/"],
"promptAppend": "Pay extra attention to the booking flow.",
"ignorePaths": ["**/legacy/**"]
}Matcher filtering
matchers: {
only: ["sql-injection", "auth-bypass"], // run *only* these
exclude: ["framework-internal-header"], // skip these
}If only is set, exclude is ignored. CLI flag --matchers <slugs> overrides the config when both are present.
Plugin order
Plugins are evaluated in array order:
plugins: [genericPlugin(), orgPlugin()]| Slot | Behavior |
|---|---|
matchers, notifiers, agents | Additive. Both plugins' contributions stack. |
ownership, people, executor | Last-write-wins. orgPlugin()'s provider replaces genericPlugin()'s. |
A monorepo gating example:
const projectId = process.argv[process.argv.indexOf("--project-id") + 1];
const isInternal = projectId?.startsWith("internal-") ?? false;
export default defineConfig({
projects: [
{ id: "internal-api", root: "../api" },
{ id: "open-source-app", root: "../app" },
],
plugins: isInternal ? [orgPlugin()] : [],
});The config file is real TypeScript. Any logic at module-load time works.
Plugin slots
| Slot | Purpose |
|---|---|
matchers | Additional regex matchers, registered alongside the built-ins. |
notifiers | Where findings get reported (Slack, GitHub Issues, webhooks, …). |
ownership | Map files to owning teams/people (e.g. an internal directory). |
people | Look up a person by email/name (managers, on-call, contact info). |
executor | Run a deepsec command on remote infrastructure. |
export interface DeepsecPlugin {
name: string;
matchers?: MatcherPlugin[];
notifiers?: NotifierPlugin[];
ownership?: OwnershipProvider;
people?: PeopleProvider;
executor?: ExecutorProvider;
agents?: AgentPluginRef[];
commands?: (program: unknown) => void; // commander program
}A single plugin can fill any subset. For details see https://github.com/vercel-labs/deepsec/blob/main/docs/plugins.md.
Models
| Backend | Default | Used by |
|---|---|---|
codex (default unless defaultAgent pins claude) | gpt-5.5 | process, revalidate |
claude | claude-opus-4-8 | process, revalidate |
claude (triage) | claude-sonnet-4-6 | triage |
CLI selection:
bunx deepsec process --agent claude --model claude-sonnet-4-6 # cheaper Claude
bunx deepsec process --agent codex --model gpt-5.4 # cheaper Codex
bunx deepsec triage --model claude-haiku-4-5 # cheaper triage--agent and --model are accepted on process, revalidate, and triage. Set the workspace-wide default via defaultAgent in deepsec.config.ts.
Environment variables
deepsec reads .env.local (auto-loaded by the CLI) or the process environment.
Required (one of)
| Var | Used by | Purpose |
|---|---|---|
AI_GATEWAY_API_KEY | all AI commands | Shortcut. Expands at startup into ANTHROPIC_AUTH_TOKEN / OPENAI_API_KEY / ANTHROPIC_BASE_URL / OPENAI_BASE_URL (one key covers Claude and Codex through Vercel AI Gateway). Any of those four set explicitly always wins. Falls back to VERCEL_OIDC_TOKEN when unset. |
ANTHROPIC_AUTH_TOKEN + ANTHROPIC_BASE_URL | process, revalidate, triage (Claude) | Direct Anthropic, or BYOK gateway-issued token. |
OPENAI_API_KEY (+ optional OPENAI_BASE_URL) | --agent codex | Codex SDK token. |
claude login / codex login session | local non-sandbox runs only | Subscription fallback. Generally lacks headroom for full scans. |
Optional
| Var | Purpose |
|---|---|
DEEPSEC_AGENT_DEBUG | Set to 1 for verbose agent logging. |
DEEPSEC_DATA_ROOT | Override the data directory (= dataDir in config). |
| Plugin-specific | Each plugin documents its own env vars in its README. |
Vercel Sandbox (optional)
For bunx deepsec sandbox …. Pick OIDC for local dev, access token for unattended CI:
# OIDC (12 h expiry, re-pull when expired)
npx vercel link
npx vercel env pull # writes VERCEL_OIDC_TOKEN
# Access token (long-lived, headless)
VERCEL_TOKEN=…
VERCEL_TEAM_ID=team_…
VERCEL_PROJECT_ID=prj_…The Sandbox SDK reads these directly from process.env at Sandbox.create() time. The SDK prefers VERCEL_OIDC_TOKEN and falls back to access-token mode otherwise.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| `Missing AI credentials for --agent claude | codex` | No credential present. |
401 Unauthorized on process / revalidate | Credential present but rejected. | OIDC: vercel env pull (12 h expiry). API key: regenerate in dashboard. Confirm .env.local is in cwd. |
Stopped: Vercel AI Gateway credits exhausted | Gateway balance is $0. | Top up at the printed URL, then re-run the same command; it resumes. |
Stopped: Anthropic API credits exhausted | Direct Anthropic out of credits. | Top up at console.anthropic.com, or switch to the gateway. |
Stopped: OpenAI API quota exhausted | Direct OpenAI out of quota. | Top up in the OpenAI dashboard, or switch to the gateway. |
Stopped: Claude Pro/Max subscription exhausted | Hit weekly / 5-hour cap. | Switch to AI Gateway. |
Stopped: ChatGPT subscription exhausted | Hit ChatGPT Plus / Pro quota. | Switch to AI Gateway. |
| Sandbox spawn fails with auth error | OIDC expired or access-token vars wrong. | vercel env pull, or verify the three access-token vars. |
| Findings missing cost in the log | Pricing entry missing for a non-default Codex model. | Add a line to MODEL_PRICING_USD_PER_M_TOKENS in packages/processor/src/agents/codex-sdk.ts (only matters if you are extending deepsec itself). |
| Persistent refusal on a single file (>5 % of batches) | Hard-to-disambiguate exploit pattern. | Add to data/<id>/config.json:ignorePaths, or run with --batch-size 1. |
After any quota / credit fix, process and revalidate resume on re-run. No recovery flag, no state to reset. Files already analyzed stay analyzed; only unfinished ones get picked up. Use --reinvestigate (process) or --force (revalidate) only when you specifically want to redo finished work.
Security model of deepsec itself
Treat deepsec like a coding agent with full shell access on the machine it runs on. It is designed to run on trusted inputs (your source code), but you may still be concerned about prompt injection from external dependencies or vendored code.
deepsec sandbox … substantially limits exposure:
- API keys are injected outside the sandbox and cannot be exfiltrated.
- Worker-sandbox network egress is locked to the configured AI host. (Egress is allowed during bootstrap, before the coding agent starts.)
Use sandbox mode for unfamiliar / vendored / contractor codebases. Local mode is fine for your own first-party code.
Matchers: author project-specific entry-point coverage
The default matcher set covers common CWE shapes (SQL injection, SSRF, path traversal, …) and a handful of popular framework shapes (Next.js, Prisma, Express, Hono, FastAPI, Django, Laravel, Rails, Gin/Echo/Fiber/Chi, …). It will miss patterns specific to your codebase: an internal RPC framework, a less common language, a custom auth helper, a non-default route layout. Custom matchers fill those gaps.
The intended loop:
scan (fast, wide) → process (AI, slow + expensive) → revalidate → write better matchersWhen to write one
- A revalidated true-positive needs a matcher to catch siblings on future scans.
- A cluster of
other-*slugs inbunx deepsec metricspoints at a real category deepsec has no name for. - The target repo has entry points the default matchers do not see. Check
https://github.com/vercel-labs/deepsec/blob/main/docs/supported-tech.mdfirst; the framework may already be covered. - You have an organization-specific pattern (internal auth helper, internal SDK call, custom middleware).
Where matchers live
.deepsec/
├── deepsec.config.ts # inline plugin lists the matchers
└── matchers/
├── my-route-no-auth.ts
└── my-internal-rpc.tsdeepsec.config.ts:
import { defineConfig, type DeepsecPlugin } from "deepsec/config";
import { myRouteNoAuth } from "./matchers/my-route-no-auth.js";
import { myInternalRpc } from "./matchers/my-internal-rpc.js";
const myPlugin: DeepsecPlugin = {
name: "my-app",
matchers: [myRouteNoAuth, myInternalRpc],
};
export default defineConfig({
projects: [{ id: "my-app", root: ".." }],
plugins: [myPlugin],
});Slugs are unique. If your slug collides with a built-in, your matcher wins. This is useful for swapping in a tighter org-specific version.
If a matcher is genuinely reusable across orgs (a CWE shape or a public-framework shape), consider upstreaming to https://github.com/vercel-labs/deepsec instead.
Workflow
1. Run scan + process first
You want real data/ to point the agent at.
bunx deepsec scan
bunx deepsec process --limit 50 # upstream-recommended calibration pass (deepsec docs)
bunx deepsec revalidate --min-severity HIGH2. Hand the workspace to the agent
Open the parent repo (the codebase being scanned) in your coding agent so it can read both source and .deepsec/data/. Then prompt:
I want to add custom matchers to deepsec for this repo. deepsec is already installed at.deepsec/node_modules/deepsec/and.deepsec/data/<projectId>/has at least one scan + process pass.
>
Read these first to understand the contract:
-.deepsec/node_modules/deepsec/dist/config.d.tsdefines theMatcherPlugininterface and theregexMatcherhelper signature.
-.deepsec/node_modules/deepsec/dist/samples/webapp/matchers/webapp-debug-flag.tsis a smallnormal-tier matcher.
- .deepsec/node_modules/deepsec/dist/samples/webapp/matchers/webapp-route-no-rate-limit.ts is a slightly larger matcher with a negative pre-check.- .deepsec/node_modules/deepsec/dist/samples/webapp/deepsec.config.ts shows how the inline plugin wires matchers into the config.>
Then do the analysis:
1. Walk.deepsec/data/<projectId>/files/and look at what the default matchers already cover. Note whichvulnSlugs show up incandidates[]and where the AI'sfindings[]ended up landing after revalidation.
2. Compare against the target repository (root above.deepsec/). Identify the major entry points: public HTTP handlers, RPC entry points, queue consumers, cron jobs, CLI commands, anything that takes untrusted input from the outside. Walk route/handler/api directories and framework config files (next.config.*,wrangler.toml,serverless.yml,Procfile,main.go,app.py, …) to figure out the entry-point shape.
3. Decide which entry points the default matchers do not reach. Common gaps:
- Frameworks deepsec does not ship a glob for (Hono, Elysia, Cloudflare Workers, Bun, Deno, FastAPI, Rails controllers, Gochi/gin, internal RPC).
- Languages with thin built-in coverage (Go, Python, Ruby, Lua, shell, Terraform, SQL).
- Custom org-specific wrappers (auth middleware, rate-limit wrappers, request-validation helpers) where deepsec's generic regexes do not know the convention.
4. Then write matchers that cover those gaps. Prefer one matcher per concern. For each:
- Slug (kebab-case, names what it flags, e.g.hono-route-no-auth,worker-fetch-handler).
- Noise tier:precise|normal|noisy(see below).
- `filePatterns` as tight as you can make them (language- or directory-anchored).
- Regex(es) that match the shape. Skip test files (.test.,.spec.,__tests__,_test.go, …).
- Save to.deepsec/matchers/<slug>.ts. Import types from"deepsec/config".
5. Wire the new matchers into the inline plugin in .deepsec/deepsec.config.ts (create the plugin if it does not exist yet).6. Runbunx deepsec scan --matchers <slug1>,<slug2>,…from.deepsec/and report how many candidates each matcher fired. Open 3 candidates per matcher to spot-check the regex is not producing obvious false positives.
>
Bias towardprecisewhen you can describe the bug exactly. Usenoisydeliberately when the goal is entry-point coverage: you would rather the AI look at every**/api/**/route.tsthan rely on a regex to predict which ones are vulnerable.
>
Generalize the shape of the pattern, not specific identifiers. If the repo's auth helper isrequireSession(), the matcher should catch any handler that does not call any session/auth helper, not the literal stringrequireSession.
3. Tune and ship
bunx deepsec scan --matchers <new-slug>Watch the candidate count:
| Tier | Sweet spot |
|---|---|
precise | 1–20 hits per 1k files |
normal | 5–100 hits per 1k files |
noisy | ≈ entry-point count of the targeted framework (10s, not 1000s) |
0 hits → too strict (loosen). >100 hits in a small repo → too loose (tighten).
When happy, commit .deepsec/deepsec.config.ts and .deepsec/matchers/. The next full scan picks them up automatically.
Noise tiers
| Tier | When | Example |
|---|---|---|
precise | Pattern is unambiguous. | prisma-raw-sql: \$queryRawUnsafe\s*\( matches only the unsafe API. |
normal | Pattern is broader; AI disambiguates. | auth-bypass: flags admin checks and skip-auth strings; AI judges. |
noisy | Every file matching a glob should be reviewed by the AI. | service-entry-point: every **/api/**/route.ts becomes a candidate. |
Tier also influences ordering. precise candidates are processed first because they have the highest signal per token.
File globs
Set filePatterns tightly. A noisy matcher with **/*.{ts,tsx} wedges the scanner on a 100k-file repo. Prefer:
- Language-specific:
**/*.go,**/*.lua,**/*.tf - Directory-anchored:
**/api/**/*.ts,**/services/**/handlers/*.ts - Combined:
**/services/**/*.{ts,go}
Worked example: covering missing entry points (FastAPI)
A team scans a FastAPI service. After a process pass, data/<id>/files/ shows the default matchers fired plenty on requirements.txt and a few *.sql files but barely touched app/routers/*.py, where the actual HTTP handlers live. The default glob set is tilted toward TypeScript/Next.js.
1. Inspect coverage. Walk data/<id>/files/app/routers/. Most FileRecords have empty candidates[]; the AI never picks them up. 2. Identify entry points. Each router decorates handlers with @router.get("/…"), @router.post("/…"), etc. The team's convention: authenticated handlers depend on a current_user: User = Depends(get_current_user) parameter. 3. Add a noisy entry-point matcher. Slug fastapi-route, noiseTier: "noisy", filePatterns: ["app/routers/**/*.py", "app/api/**/*.py"], regex /@\w+\.(get|post|put|delete|patch)\s*\(/. Every router file becomes a candidate; the AI reads them on the next process pass. 4. Add a precise auth-shape matcher. Slug fastapi-route-no-auth, noiseTier: "precise", same globs, regex sweep for @\w+\.(get|post|...) whose subsequent def/async def signature lacks Depends(get_current_user) or Depends(require_*).
Result on the next scan: the AI investigates every router file, and the precise matcher flags handlers that skip the auth dependency.
Generic vs plugin vs upstream contribution
| Catches… | Where |
|---|---|
| An org-specific helper, package, or route layout | Your inline plugin (.deepsec/matchers/) |
| A reference to a concrete internal service name | Your inline plugin |
| A CWE shape (path traversal, SSRF, prototype pollution) the public set misses | Consider upstreaming to https://github.com/vercel-labs/deepsec |
| A shape for a popular OSS framework (Hono, FastAPI, Drizzle) | Upstreaming benefits everyone |
For copy-paste starting points, see .deepsec/node_modules/deepsec/dist/samples/webapp/matchers/.
PR review: process --diff for CI gating
Use direct mode when you want a fast, scoped read of the files changed in a PR rather than a whole-repo audit.
bunx deepsec process --diff origin/mainHow direct mode differs from a full scan
| Step | What it looks at | What it produces |
|---|---|---|
| Resolve files | --diff / --diff-staged / --diff-working / --files / --files-from | POSIX-relative file list under rootPath |
| Scoped scan | Only the listed files | Candidates as prompt signals (best-effort) |
| Always-process | The same listed files | AI findings, including files no matcher hit |
Files with no regex hits still get a record and still get investigated as a holistic review.
Diff sources (mutually exclusive)
| Flag | Meaning |
|---|---|
| `--diff <ref | range>` |
--diff-staged | Index vs HEAD |
--diff-working | Uncommitted + untracked |
--files <csv> | Explicit comma-separated list |
--files-from <path> | Newline-delimited list (or - for stdin) |
Other knobs:
| Flag | Effect |
|---|---|
--no-ignore | Bypass the default ignore filter (test files, dist/, node_modules/, …) |
--comment-out <path> | Write a PR-comment-shaped markdown summary to <path> (only when findings exist) |
--project-id <id> | Override project id (auto-derived from rootPath basename otherwise) |
--root <path> | Override project root |
The usual --agent, --model, --concurrency, --batch-size, --max-turns flags work the same as in standard mode.
Auto-created projects
You do not need to run deepsec init first. With a direct-mode flag, process will:
1. Use --project-id if you pass one (if declared in deepsec.config.ts, the declared root is used; otherwise --root or cwd). 2. Otherwise derive the id from the resolved root's basename. 3. Write data/<id>/project.json if absent.
Auto-creation is one-line and non-destructive. It never modifies your deepsec.config.ts.
Exit codes (gating contract)
| Code | Meaning |
|---|---|
0 | No findings produced in this run |
1 | At least one net-new finding produced |
| other | Runtime error (bad input, missing credentials, …) |
Net-new findings only count toward the exit code. Re-running on a file with existing findings does not fail the build unless something new is surfaced. Pre-existing findings on touched files are intentionally excluded.
PR-comment markdown
--comment-out <path> writes a markdown body summarizing the net-new findings only (same scope as the exit-code gate). Descriptions and recommendations are truncated (600 / 400 chars) to stay under GitHub's 65 KiB comment limit; full text remains in data/<id>/files/.
The file is only written when there are findings, so a green run leaves nothing on disk and your "post comment" step can short-circuit on if: hashFiles('comment.md') != ''.
Two-job CI pattern (recommended)
Keep PR-controlled code in a no-write job; let a second, code-free job post the comment.
name: deepsec
on: pull_request
permissions:
contents: read
jobs:
analyze:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need history for `git diff origin/<base>`
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 24, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: npm install -g @anthropic-ai/claude-code
- id: deepsec
env:
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}
CLAUDE_CODE_EXECUTABLE: claude
run: |
pnpm deepsec process \
--diff origin/${{ github.event.pull_request.base.ref }} \
--comment-out comment.md
- if: always() && hashFiles('comment.md') != ''
uses: actions/upload-artifact@v4
with:
name: deepsec-comment
path: comment.md
retention-days: 1
comment:
needs: analyze
if: always() && needs.analyze.result == 'failure'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
steps:
- id: dl
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: deepsec-comment
- if: steps.dl.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: fs.readFileSync('comment.md', 'utf8'),
});Swappnpm deepsecforbunx deepsec/npx -y deepsec/yarn deepsecto match the project's package manager. If usingbun, replace thepnpm/action-setup+setup-node(cache: pnpm)block withoven-sh/setup-bunandbun install --frozen-lockfile.
Why the split
- `analyze` runs PR-controlled code (the user's
pnpm install, their config, their source) with the AI gateway secret in scope but no write permissions on the repo. - `comment` has
pull-requests: writebut never runs any PR code; it consumes only the sanitizedcomment.mdartifact. - A malicious PR cannot combine "execute arbitrary code" with "write to the repository" in a single privileged step.
Threat-model notes
- Do not grant `pull-requests: write` to a job that runs PR code. A PR can add arbitrary code to its own
package.jsonpostinstall scripts or to a project config the CLI loads. Both run before any of your steps. - Pin actions to full SHAs in production. The example uses major-version tags for readability. Swap each tag for the action's full commit SHA so a compromised tag cannot pivot into your secret-bearing job. (See GitHub's hardening guide.)
- Same-repo-only gate (
if: github.event.pull_request.head.repo.full_name == github.repository) skips fork PRs, which already do not receive secrets underpull_request. Pure UX cleanup. - The AI gateway secret still flows through PR code in
analyze. Theauthor_association/ same-repo gate is what prevents that from being a vulnerability. For defense-in-depth, runanalyzeonly after a label is applied:
if: contains(github.event.pull_request.labels.*.name, 'review-ok')Cost notes
Wide diffs are expensive: every file pays for an AI investigation.
- For PRs against
main, scope to the merge base (origin/main), not the entire branch ancestry. - Drop generated / fixture files via
--files-from:
git diff --name-only origin/main \
| grep -v '^generated/' \
| bunx deepsec process --files-from -- Add stable noise paths to ignore patterns in
data/<id>/config.json:ignorePathsso they never enter the diff.
When NOT to use direct mode
- Initial sweep of a large repo. Full
scan+processorders by noise tier, parallelizes better, and benefits from whole-repo signal in matcher gating. Direct mode is for incremental review. - Revalidating existing findings. Use
revalidatewith its own filters.
Scanning: scan → process → triage → revalidate → export
All commands run from inside .deepsec/. bunx deepsec … is interchangeable with pnpm deepsec …, npm exec deepsec …, yarn deepsec ….
Pipeline
scan process revalidate enrich export / report / metrics
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
candidates → findings TP/FP/Fixed verdict → +committers JSON / md-dir / aggregate
+ownershipStages are idempotent and additive. Re-running merges new info instead of overwriting. State lives under data/<id>/.
Calibration first (mandatory on > 500-file repos)
The deepsec docs (getting-started.md, vercel-setup.md, faq.md) recommend --limit 50 --concurrency 5 as the calibration starting point. Defer to a user-named value if given.
bunx deepsec scan
bunx deepsec status # show pending / scanned counts
bunx deepsec process --limit 50 --concurrency 5 # upstream-recommended calibrationscan runs ~110 regex matchers across the codebase. No AI calls. ~15s on 2k files. Output goes to data/<id>/files/ as one FileRecord JSON per scanned source file.
The calibration process is a budget-capped AI pass. Read the per-batch cost the CLI prints, multiply by (total_files / 50) to extrapolate. Get the user's explicit go-ahead before launching the unbounded `process`.
Cost guide (--agent claude, Claude Opus — the most expensive backend)
| Files | Approx cost | Approx wall time |
|---|---|---|
| 100 | $25–60 | 5–15 min |
| 500 | $130–300 | 25–60 min |
| 2,000 | $500–1,200 | 1.5–4 hr |
Costs swing 2–3× based on file complexity. Codex is cheaper per call; Opus is the precision benchmark.
Full investigation
bunx deepsec process --concurrency 5Defaults: --agent codex (gpt-5.5) unless defaultAgent in deepsec.config.ts pins otherwise; --agent claude uses claude-opus-4-8. --batch-size 5; --concurrency defaults to cores−1 (the cost guide above assumes --concurrency 5 ⇒ 25 files in flight at peak). Files are claimed atomically via lockedByRunId; multiple workers can run in parallel without stepping on each other.
For the precision (most expensive) backend:
bunx deepsec process --agent claudeCodex (the default) runs in a strict read-only sandbox and is fast at grep-heavy investigations. Backends mix freely within a project: re-process unconvincing findings with the other agent, and findings dedupe across agents.
Resume after interruption
process and revalidate are safe to re-run. Network blip, transient model error, quota stop, Ctrl-C → re-run the same command. Files already finished are skipped. Nothing to clean up. Never rm -rf data/<id>/ to "start clean" without explicit user instruction.
Reinvestigate finished work
Use --reinvestigate (entire repo) or --reinvestigate <N> (wave marker) when a stronger model lands or you want a second opinion. Findings dedupe across agents; the new analysis appends to analysisHistory rather than overwriting.
Triage and revalidate
bunx deepsec triage --severity HIGH
bunx deepsec revalidate --min-severity HIGH| Stage | What | Cost |
|---|---|---|
triage | Classifies findings P0/P1/P2/skip from finding text only (no code re-read). Claude Sonnet by default. | ~$0.01 / finding |
revalidate | Re-reads code + git history, emits true-positive / false-positive / fixed / uncertain verdicts and may adjust severity. | Comparable to process |
revalidate empirically cuts FP rate by 50%+ on most repos. Run it on HIGH+ before surfacing anything to the user.
Export
bunx deepsec export --format md-dir --out ./findings # one .md per finding under {CRITICAL,HIGH,…}/
bunx deepsec export --format json --out findings.json # single JSON array, pipe-friendly
bunx deepsec metrics # aggregate counts, severities, TP rates
bunx deepsec report # per-project markdown + JSON summaryEach command takes --project-id <id> if your config has multiple projects.
Useful flags
| Flag | Purpose |
|---|---|
--limit <N> | Cap files processed in this run. |
--concurrency <N> | Parallel batches in flight. Lower for laptop-friendliness or quota-friendliness. |
--batch-size <N> | Files per batch (default 5). |
--max-turns <N> | Cap agent conversation turns per batch. |
| `--agent claude | codex` |
--model <id> | Override per-backend model (claude-sonnet-4-6, gpt-5.5-pro, claude-haiku-4-5, …). |
--matchers <slugs> | CSV of slugs; restricts the matcher set on scan. Overrides matchers.only in config when both are set. |
--reinvestigate / --reinvestigate <N> | Force re-analysis on process. |
--force | Force re-analysis on revalidate. |
--project-id <id> | Pick a project when more than one is registered. |
--root <path> | Override project root for one-off scans. |
Reading data/ directly
data/<id>/files/**/*.json are FileRecords. Useful jq one-liners:
# All TP HIGH+ findings
jq -r '. as $r | $r.findings[] | select(.revalidation.verdict=="true-positive") | select(.severity=="HIGH" or .severity=="CRITICAL") | [$r.filePath, .severity, .title] | @tsv' data/<id>/files/**/*.json
# Total spend on this project
jq -s 'map(.analysisHistory[].costUsd // 0) | add' data/<id>/files/**/*.json
# Files still pending after the latest run
jq -r 'select(.status=="pending") | .filePath' data/<id>/files/**/*.jsonFor richer queries, prefer bunx deepsec export --format json. Its filters match the rest of the CLI.
Cron / scheduled CI
# Sunday cron: full scan
bunx deepsec scan
bunx deepsec process --concurrency 5
bunx deepsec revalidate --min-severity HIGH
bunx deepsec export --format json --out findings.jsonPersist .deepsec/data/ between runs (cache it as a build artifact) or re-scan from scratch each time. The append-only model means cached data/ strictly improves cost on the next run.
Distributed (sandbox)
Large monorepos can fan work across Vercel Sandbox microVMs:
bunx deepsec sandbox process --project-id my-app --sandboxes 10 --concurrency 4Local working tree is tarballed (.git excluded) and uploaded. Sandbox-level network egress is locked to the configured AI host(s); the gateway key is injected outside the sandbox so it cannot be exfiltrated. Use this when the repo is large enough that local concurrency saturates your machine, or when running unattended in CI/CD.
See config.md for Sandbox auth (OIDC vs access token).
What process does not do
- Does not modify source code. Findings are advisory.
- Does not commit / push / open PRs. Hand off to
oma-scmif the user wants commits. - Does not call out to non-AI external services unless a notifier plugin is configured.
- Does not phone home or report telemetry;
data/<id>/stays on your machine unless explicitly exported.
Setup: install .deepsec/ and bootstrap INFO.md
1. Install the workspace
Requires Node.js 22+. Run from the root of the codebase you want to scan:
bunx deepsec init # creates .deepsec/ and registers this repo
cd .deepsec
bun install # installs deepsec from npm
# pnpm / npm / yarn equivalents work the same way:
# npx deepsec init && cd .deepsec && pnpm install
# npx deepsec init && cd .deepsec && npm install
# npx deepsec init && cd .deepsec && yarn installinit lays down a minimal scaffold inside .deepsec/:
package.jsondeepsec.config.ts(oneprojects[]entry pointing at.., id derived from the parent dir's basename)data/<id>/INFO.md(template with section placeholders)data/<id>/SETUP.md(per-project agent prompt)- workspace-level
AGENTS.md .env.local.gitignore(keepsINFO.md,SETUP.md,deepsec.config.tstracked; ignoresdata/*/files/,data/*/runs/, etc.)
No custom matchers in the scaffold. Add those only when a real finding shapes one for you.
To scan another codebase from the same.deepsec/:bunx deepsec init-project <path>(relative paths resolve against.deepsec/'s parent).
2. Pick a credential
Open .deepsec/.env.local and pick one:
| Mode | When | Set |
|---|---|---|
| AI Gateway API key | Anywhere, simplest | AI_GATEWAY_API_KEY=vck_… from the Vercel AI Gateway API Keys page |
| Vercel OIDC token | Already linked to a Vercel project (or using Sandbox) | npx vercel link && npx vercel env pull writes VERCEL_OIDC_TOKEN (12 h expiry; re-pull on auth errors) |
| Direct Anthropic | BYOK / bypass gateway | ANTHROPIC_AUTH_TOKEN=sk-ant-… + ANTHROPIC_BASE_URL=https://api.anthropic.com |
| Direct OpenAI | Codex backend, BYOK | OPENAI_API_KEY=sk-… (+ OPENAI_BASE_URL only for proxies) |
| Subscription | Local-only evaluation | claude login and/or codex login already done; non-sandbox runs reuse the session, no token needed |
AI_GATEWAY_API_KEY expands at CLI startup into ANTHROPIC_AUTH_TOKEN / OPENAI_API_KEY / ANTHROPIC_BASE_URL / OPENAI_BASE_URL. Any of those four set explicitly always wins.
Subscriptions are useful for evaluating deepsec but generally do not have enough headroom for full repo scans. Switch to the gateway once past evaluation.
3. Verify the credential
bunx deepsec scan --limit 20 # cheap, no AI calls
bunx deepsec process --limit 5 # exercises the gatewayIf the second call returns Missing AI credentials or 401, see config.md § Troubleshooting.
4. Write INFO.md (do not skip)
INFO.md is what makes deepsec project-aware. It is injected into the AI prompt for every batch, so vague content here means vague findings.
Recommended: agent-driven write-up
Open the parent repo (the codebase you scanned, not .deepsec/) in your coding agent and paste the prompt that deepsec init printed (also in the project root README):
Read.deepsec/node_modules/deepsec/SKILL.mdto understand the tool. Then read.deepsec/data/<id>/SETUP.mdand follow it: skim this repo's README, anyAGENTS.md/CLAUDE.md, and a handful of representative code files, then replace each section of.deepsec/data/<id>/INFO.md.
>
Keep it SHORT: target 50–100 lines total. Pick 3–5 examples per section, not exhaustive enumeration. Name primitives (auth helpers, middleware) but no line numbers. Skip generic CWE categories; built-in matchers cover those. Cover only what is project-specific. INFO.md is injected into every scan batch; verbose context dilutes signal.Manual write-up
The processor auto-loads data/<id>/INFO.md from the workspace's data dir. Edit it directly; no extra wiring is needed in deepsec.config.ts. Even a single tight paragraph noticeably improves the AI's output.
What goes in INFO.md
Project-specific only:
- What the codebase does in a few sentences.
- Auth shape: names of helpers / middleware / decorators that gate access (
requireSession,Depends(get_current_user), etc.). Name them, do not quote them. - Threat model: which surfaces matter (public HTTP, internal RPC, queue consumers, cron, CLI) and which are out of scope.
- Known FP sources: patterns the AI tends to over-flag in this repo.
- Project-specific primitives: internal SDK calls, custom validators, codified secret-loading paths.
- Out of scope: directories or file types the AI should ignore.
What stays out
- Generic CWE category descriptions; built-in matchers cover those.
- Exhaustive enumeration. Pick 3–5 representative examples per section.
- Line numbers. They drift; the AI re-reads files anyway.
- Boilerplate intro paragraphs.
5. .gitignore hygiene
The scaffold's .deepsec/.gitignore already keeps INFO.md, SETUP.md, and deepsec.config.ts tracked (so teammates inherit project context) and ignores generated state. Do not unignore data/*/files/ or data/*/runs/ unless you have a deliberate reason (e.g. CI cache).
.env.local must stay gitignored. Never commit vck_…, sk-ant-…, sk-…, or OIDC tokens.
6. Multi-project workspaces
To scan a different codebase from the same .deepsec/:
bunx deepsec init-project <path>Each project gets its own data/<id>/ subdirectory. Pass --project-id <id> to disambiguate any subsequent command (auto-resolution only kicks in with exactly one project).
7. Sanity check before the first real run
- [ ]
.deepsec/.env.localhas a working credential. - [ ]
bunx deepsec scan --limit 20succeeds. - [ ]
bunx deepsec process --limit 5succeeds and prints a per-batch cost number. - [ ]
data/<id>/INFO.mdis filled in (50-100 lines, project-specific). - [ ] You and the user agree on a calibration scope for the first
processrun (deepsec docs default:--limit 50 --concurrency 5).
Triage: read findings, cut false positives, prioritize work
Severity vocabulary
| Severity | Meaning |
|---|---|
CRITICAL | Pre-auth or trivially exploitable issue with broad blast radius. |
HIGH | Real vulnerability, likely exploitable in this codebase's context. |
MEDIUM | Conditional vulnerability or one with significant attacker prerequisites. |
LOW | Defense-in-depth gap, or correctness issue with weak security framing. |
HIGH_BUG / BUG | Real bug the agent declined to call a vulnerability; typically correctness with security-adjacent risk. |
triage adds a priority field on top of severity:
| Priority | Trigger |
|---|---|
P0 | Drop everything; the exploit is trivial and the impact is critical. |
P1 | This sprint. |
P2 | Backlog. |
skip | Not worth fixing (nuance, intended behavior, false alarm). |
Read order
Do not show the user raw process output for HIGH+ findings. The right pipeline is:
1. bunx deepsec process (or process --diff for PR mode). 2. bunx deepsec triage --severity HIGH to bucket findings into P0/P1/P2 (~$0.01 / finding). 3. bunx deepsec revalidate --min-severity HIGH re-reads the code and git history, then emits a verdict. The cost is comparable to process, and FP rate drops by 50%+. 4. bunx deepsec export --format md-dir --out ./findings to surface results to the user.
revalidate verdicts:
| Verdict | Action |
|---|---|
true-positive | Fix it. Hand off to oma-debug or the matching domain skill. |
false-positive | Note in INFO.md if the FP shape is recurring. Adjust matchers if it is a regex-level over-match. |
fixed | The finding refers to code that was already patched in git history; no action. |
uncertain | Re-run with the other agent, or escalate to a human reviewer. |
Cutting FP rate
Two things help most:
1. Always `revalidate` before acting on `HIGH+`. Worth the cost. 2. Tighten `INFO.md`. Even one paragraph about the auth shape, threat model, and known FP sources improves precision a lot. See setup.md § 4.
After revalidation, FP rate on HIGH+ typically lands in the 10–29 % range.
Refusals
Models occasionally refuse to investigate a candidate (exploit-shaped source, content filter). After every batch deepsec asks the agent whether anything was skipped; refused: true appears in RunMeta and on the FileRecord.refusal field. The per-batch log shows a refusal marker.
Handling:
- A refused batch produces no false negatives. Affected files stay
pending, so re-run--reinvestigateagainst the other backend (Claude ↔ Codex) to pick up the dropped sites. Findings dedupe across agents. - If a single file consistently triggers refusals (>5 % of batches), add it to
data/<id>/config.json:ignorePaths, or run that file alone with--batch-size 1so a refusal does not take an otherwise-fine batch down with it. - Never silently drop a refusal. Document it in the user-facing summary.
Reading severity counts
bunx deepsec metricsShows cross-project counts: severities, vulns by type, TPs after revalidation. Use it to decide where matcher investment pays off (clusters of other-* slugs are the strongest signal).
Per-finding markdown export shape
bunx deepsec export --format md-dir --out ./findings produces:
findings/
├── CRITICAL/
├── HIGH/
├── MEDIUM/
├── LOW/
└── BUG/Each file contains: severity, title, vulnSlug, file path with line numbers, description, recommendation, confidence, triage verdict (if run), revalidation verdict (if run), and an analysisHistory summary. Use these as inputs to issue tracker tickets; the structure is friendly to GitHub Issues / Linear / Jira import scripts.
When to not surface a finding
revalidation.verdict === "false-positive".revalidation.verdict === "fixed"and the fix matches the currentHEAD.triage.priority === "skip"with reasoning the user agrees with.- Severity below the user-stated
severity_floor.
For everything else: surface it, with verdict, recommendation, and the file path.
Hand-off
Route by the layer of the vulnerable file, judged from each finding's filePath + vulnSlug + revalidation.verdict against the project's own signals: data/<id>/tech.json, INFO.md, priorityPaths, and the actual directory structure. Do not bake a slug or path enumeration into this skill. Deepsec evolves its matcher set and project layouts vary, so trust the artifact at runtime.
| Layer of the vulnerable file | Specialist |
|---|---|
| Backend / server / API | oma-backend |
| Frontend / web client | oma-frontend |
| Mobile / native client | oma-mobile |
| IaC / cloud / network | oma-tf-infra |
| Database / data model | oma-db |
| CI / workflow / supply chain | oma-dev-workflow |
| Documentation drift surfaced by the run | oma-docs |
Ambiguity → `oma-debug` first. Route to oma-debug whenever the layer is not obvious from the artifact: shared / isomorphic / utility code, an other-* slug, a fix that would touch multiple layers, revalidation.verdict === "uncertain", or BUG / HIGH_BUG non-security correctness without an obvious owner. The hop is triage, not fix: pin the exact file:line and re-route to the right specialist with a layer-tagged finding. Fix inline only when the change is a single isolated line and the diagnosis is confident. Record the second-hop owner in the run summary.
Attach to every routed item: file path, severity, vulnSlug, revalidation verdict, recommendation, and the export markdown path.