File: README.md
deepsec
deepsec is an agent-powered vulnerability scanner that you can run in your own infrastructure, optimized to perform on-demand review of all code in existing
large-scale repos.
deepsec is designed to surface hard-to-find issues that have been lurking in applications for a long time. It is configured to use the best models at maximum thinking levels (tunable via --thinking-level, see models), meaning scans can cost thousands or even tens-of-thousands of dollars for large codebases. Our customers have found the cost worth it for how quickly they were able to patch vulnerabilities that would have otherwise gone unfixed.
For large codebases, work fans out across worker machines in parallel.
If a run is interrupted or errors out partway through, just re-run the same
command โ deepsec picks up where it left off, skipping files it already
analyzed and only investigating the rest.
Get started
From the root of the repository you want to scan:
npx deepsec initThe command guides you through everything. It asks you to pick an AI model
(with benchmark scores and prices to compare) and how to pay for model
usage โ your own OpenAI/Anthropic API key, or Vercel AI Gateway โ and then
works unattended: it studies your codebase, scans it, and runs the AI
review. The only thing it adds to your repository is a .deepsec/ folder
where all of its state and findings live.
If the run is interrupted for any reason โ Ctrl-C, lost connection, a
spending limit โ run npx deepsec init again and it continues where it
left off. To cap what a run may spend or how long it may take:
npx deepsec init --max-cost-usd 100 --max-duration 2hWhen the scan finishes, get a readable report:
cd .deepsec
pnpm deepsec export --format md-dir --out ./findingsFor later scans, work from inside .deepsec/:
pnpm deepsec scan # fast pattern scan, free
pnpm deepsec process # AI review of new candidates
pnpm deepsec revalidate # optional, cuts false-positive rate
pnpm deepsec export --format md-dir --out ./findingsThe getting started guide
covers all of this in more detail, including using your own OpenAI or
Anthropic API key and running from CI or a coding agent.
Docs
After initialization, agents can read the exact documentation matching the
installed CLI at .deepsec/node_modules/deepsec/SKILL.md and.deepsec/node_modules/deepsec/dist/docs/. Setup errors expose these as
absolute machine-readable paths.
- Getting started โ set up and run your first scan
- Reviewing changes โ
process --diffand CI gating - Supported technology โ built-in coverage
- Generated and hand-authored matchers
- Configuration
- Plugins
- Models
- Project link and credentials
- Architecture
- Data layout
- FAQ
- Samples
- Contributing
AI provider
By default, deepsec routes model calls through Vercel AI Gateway, which
gives access to every major model without provider-specific keys. You can
instead bring your own key โ OpenAI, Anthropic, or a custom HTTPS
provider โ by passing --model-auth direct with --ai-provider and--ai-api-key-env to init; no Vercel account is needed in that mode.
Deepsec only ever stores the name of the environment variable holding
your key, never the key itself. See
project link and credentials
for the full reference.
If a process or revalidate run halts because the upstream credential
ran out of quota or credits, deepsec stops gracefully and tells you
where to top up. Re-run the same command afterward and it picks up
where it left off.
Distributed execution (optional)
Large monorepos can fan work across Vercel Sandbox microVMs:
pnpm deepsec sandbox process --project-id my-app --sandboxes 10 --concurrency 4Setup already verified the Vercel connection, so this needs no extra
onboarding. The local working tree is tarballed and uploaded; .git is
excluded. Model credentials remain host-side and are injected only at the
selected egress host.
Security model of deepsec itself
Treat deepsec like a coding agent with full shell access on the enviroment that it is
running on. It is designed to run on trusted inputs (your source code) but you may still
be concerned about prompt injection due to external dependencies or vendored code.
Running on a sandbox (see above) does limit the potential exposure substantially:
- The API keys for the coding agents are injected outside of the sandbox and hence cannot be exfiltrated
- For the worker sandboxes, network egress from the sandbox is limited to coding agent hosts (Egress is allowed during the bootstrap process, but this does not run the coding agent)
Workflow reference
| Command | What it does |
|---|---|
scan |
Find candidate sites with regex matchers (fast, no AI) |
process |
AI investigation; emits findings + recommendation |
process --diff |
PR-mode: scan + investigate only files changed in a diff |
triage |
Lightweight P0/P1/P2 classification (cheaper model) |
revalidate |
Re-check existing findings; checks git history for fixes |
enrich |
Add git committer info + (with a plugin) ownership data |
report |
Markdown + JSON summary for one project |
export |
Per-finding JSON or directory of markdown files |
metrics |
Cross-project counts: severities, vulns by type, TPs |
status |
Snapshot of the project mirror |
sandbox <cmd> |
Run any of the above on Vercel Sandbox microVMs |
License
Apache 2.0. See LICENSE and NOTICE.
File: docs/architecture.md
title: "Architecture"
Initialization and steady-state pipeline
scaffold โ install โ link/model/Sandbox โ INFO + inventory
โ
โผ
baseline scan โ coverage policy
โ gap
โผ
declarative matcher generation
โ
โโโโ final scan โ process
steady state: scan โ process โ revalidate โ enrich โ export/reportdeepsec init and deepsec setup coordinate the initialization graph.
Each phase writes an input digest and output checkpoint; a retry skips a
current completed phase and resumes at the first missing or invalid output.
Steady-state stages remain separate CLI subcommands and use the same on-disk
representation.
Install and auth have two levels of idempotency. Setup state avoids expensive
work, while cheap probes still confirm node_modules/deepsec exists and
rehydrate the configured model credential. The auth layer independently
short-circuits fresh model and Sandbox probes when the exact project link,
route, and agent set are unchanged.
On-disk layout
data//
โโโ project.json # rootPath, githubUrl (auto-managed)
โโโ INFO.md # repo context injected into AI prompts (manual or agent-written)
โโโ config.json # priorityPaths, promptAppend, ignorePaths (optional)
โโโ setup/ # generated setup evidence (gitignored)
โ โโโ setup-state.json # phase checkpoints, digests, run IDs
โ โโโ surface-inventory.json# structured ingress inventory
โโโ files/ # one JSON per scanned file (FileRecord)
โ โโโ path/to/file.ts.json
โโโ runs/ # one JSON per run (RunMeta)
โ โโโ 20260429-abcd.json
โโโ reports/ # generated reports (markdown + JSON)Generated files/, runs/, reports/, project.json, and setup/ are
gitignored by the scaffold; INFO.md remains trackable. Each FileRecord is the source of truth
for everything deepsec knows about a single source file: candidate
matches, AI findings, analysis history, git committer info, ownership.
Full schemas for every file under data/ are documented in
data-layout.
The merge model is additive: every stage adds to the FileRecord. A
re-scan merges new candidates into the existing set; a re-process appends
to analysisHistory and merges new findings; revalidation tags existing
findings with verdicts. Nothing is overwritten or deleted.
Stage details
setup coordinator
- Repository analysis: a read-only agent produces concise
INFO.mdplus
a validated structured surface inventory. Invalid output receives one
repair attempt. - Coverage: inventory globs expand against the scanner's ignored-file
universe. The evaluator checks representative files, broad surface ratios,
sensitive zero-coverage surfaces, dominant-language blind spots, and new
matcher breadth. - Generated matchers: model output is strict JSON data compiled without
evaluating generated code. Regex/glob complexity, examples, slug collisions,
traversal, and match explosions are rejected. Accepted specs are written togenerated-matchers.tsfor review and commit. - Gate: setup performs at most two generation/rescan attempts and never
starts paid processing while coverage still fails.
The setup agent runner supports Codex, Claude, and Pi through a small
read-only task interface separate from the investigation interface.
scan
- What it does: Glob the project root, run regex matchers on every
matched file, writecandidatesto each FileRecord. - Cost: Free (no AI). ~15s for 2k files.
- Inputs: Project root, matcher set (built-ins + plugin contributions).
- Outputs:
data//files/**/*.jsonwithcandidatespopulated andstatus: "pending".
The matcher set is built per-run from the default registry plus any matchers
contributed by active plugins, including the generated matcher plugin.
Generated specs must use unique slugs; collisions are rejected before scan.
process
- What it does: Pick batches of pending files, send each batch to the
configured AI agent backend with the system prompt + INFO.md, parse the
agent's JSON response intoFindings, write them back to each FileRecord. - Cost: $$. The expensive stage.
- Inputs: FileRecords with
status: "pending",INFO.md, the prompt
template (packages/processor/src/index.ts:DEFAULT_PROMPT_TEMPLATE). - Outputs: FileRecord
findings[]populated,status: "analyzed",analysisHistory[]appended.
Three agent backends are supported:
--agent |
SDK | Default model |
|---|---|---|
codex (default) |
@openai/codex-sdk |
gpt-5.5 |
claude |
@anthropic-ai/claude-agent-sdk |
claude-opus-4-8 |
pi |
@earendil-works/pi-coding-agent |
zai/glm-5.2 |
Same prompt, same JSON output schema. You can mix backends within a
project โ re-process a file with a different agent and the second run's
findings get merged with the first.
Concurrency: --concurrency 5 --batch-size 5 means 5 batches in flight,
5 files per batch = 25 files in the air at peak. The processor claims
files atomically via lockedByRunId so multiple workers can run in
parallel without stepping on each other.
revalidate
- What it does: Re-check existing findings for false positives. The
agent re-reads the code, consults git history (was this fixed?), and
emits a verdict:true-positive,false-positive,fixed, oruncertain. - Cost: $$. Comparable to
process. Worth running on HIGH+. - Inputs: Findings with no
revalidationfield, or with--force. - Outputs:
revalidation: { verdict, reasoning, โฆ }on each finding.
Empirically reduces FP rate by 50%+ on most repos.
enrich
- What it does: Attach git committer info and (with a plugin)
ownership data to FileRecords with findings. - Cost: Free if no ownership plugin; otherwise one HTTP round-trip
per file to the ownership provider. - Inputs: FileRecords with findings, the project's git history.
- Outputs:
gitInfo: { recentCommitters, ownership }on each record.
export / report / metrics
Read-only stages. Don't modify FileRecords; just shape the data for human
or downstream consumption.
- export โ flat list of findings as JSON or directory of markdown.
- report โ per-project markdown summary + JSON.
- metrics โ cross-project counts and TP rates.
Plugin architecture
Five extension points, all defined inpackages/core/src/plugin.ts:
matchersโ additivenotifiersโ additiveagentsโ additiveownershipโ single-slot (last plugin wins)peopleโ single-slotexecutorโ single-slot
A plugin registers via deepsec.config.ts:
export default defineConfig({
plugins: [vercel(), myPlugin()],
});The CLI calls loadConfig() before parsing args, builds a PluginRegistry
from the active plugins, and stashes it on a module-level singleton
(getRegistry()). All internal code consults the registry rather than
hard-coding integrations.
See plugins for the full plugin authoring guide.
Design decisions
One file = one FileRecord. The unit of work is a source file, not
a finding. Scanner, processor, and revalidator all operate on files,
so atomic per-file locking and idempotent merges fall out naturally.Append-only analysis history. Re-running the processor doesn't
overwrite past findings. It appends a new entry toanalysisHistory
and merges new findings (deduped by slug + title) intofindings. You
can re-run with a different agent, prompt, or model and get a strict
improvement instead of a destructive replacement.Plugin-mediated integrations. Matchers, notifiers, ownership
sources, and the remote executor all sit behind plugin contracts. The
open-source release ships with a generic core; organization-specific
matchers, notifiers, ownership oracles, and people directories slot
in as external plugins.
File: docs/configuration.md
title: "Configuration reference"
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({
ai: { mode: "gateway", provider: "vercel" },
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.
Top-level fields
| Field | Type | Purpose |
|---|---|---|
projects |
ProjectDeclaration[] |
The 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 |
string |
Default --agent value (codex, claude, or pi). See models. |
defaultModel |
string |
Default --model value selected during setup. |
defaultThinkingLevel |
string |
Default reasoning effort (minimal through xhigh) selected during setup. |
ai |
ModelRoute |
Non-secret model credential route selected and verified by setup. |
dataDir |
string |
Override the data/ directory. Defaults to ./data. |
Model route
One-shot setup persists how later AI commands should find their credential,
never the credential value itself.
// Default: linked-project OIDC or AI_GATEWAY_API_KEY
ai: { mode: "gateway", provider: "vercel" }
// User-owned OpenAI key; MY_OPENAI_KEY must exist at runtime
ai: {
mode: "direct",
provider: "openai",
apiKeyEnv: "MY_OPENAI_KEY",
baseUrl: "https://api.openai.com/v1",
}
// Pi-only custom provider
ai: {
mode: "custom",
provider: "martian",
apiKeyEnv: "MARTIAN_KEY",
baseUrl: "https://api.martian.example/v1",
credentialHeader: { name: "x-api-key", scheme: "raw" },
}process, revalidate, setup, and Sandbox orchestration resolve this
route in each fresh process. Explicit per-command Pi provider flags take
precedence. Run deepsec setup --model-auth โฆ to change and verify the route
instead of hand-editing it.
ProjectDeclaration
| Field | Type | Required | Purpose |
|---|---|---|---|
id |
string |
yes | Used as --project-id and the data directory name (data//). |
root |
string |
yes | Absolute or relative path to the codebase. |
githubUrl |
string |
no | https://github.com/owner/repo/blob/branch โ used in exports for clickable links. Auto-detected from git remote when omitted. |
infoMarkdown |
string |
no | Repo context injected into AI prompts. Overrides data//INFO.md if both are present. |
promptAppend |
string |
no | Free-form text appended to the system prompt for this project. |
priorityPaths |
string[] |
no | Path prefixes to process first. |
INFO.md
If infoMarkdown isn't set in the config, deepsec looks fordata//INFO.md and injects its contents into the prompt forprocess, triage, and revalidate. A few hundred words of repo
context (what the codebase does, the auth shape, the threat model,
known false-positive sources) is the right length. One-shot setup writes and
validates this file automatically. See
getting-started for its required sections and resume
behavior.
Generated matchers
New workspaces import generatedMatchersPlugin fromgenerated-matchers.ts. Setup writes accepted data-only matcher specs into
that file after schema, example, regex-safety, duplicate-slug, coverage, and
breadth validation. Keep the import/plugin entry in config and commit the
generated file after review.
Hand-authored plugins remain additive and can live beside the generated
plugin. See writing-matchers.
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()]For matchers, notifiers, agents: additive โ both plugins'
contributions are registered.
For ownership, people, executor: last-write-wins โ orgPlugin()'s
provider replaces genericPlugin()'s.
Per-project config files
Some legacy fields still live in data//config.json:
{
"priorityPaths": ["app/api/", "lib/"],
"promptAppend": "Pay extra attention to the booking flow.",
"ignorePaths": ["**/legacy/**"]
}This is read by scan and by the AI agents. It overrides the same fields
on the project declaration if both are present.
Environment variables
deepsec reads these from .env.local (loaded automatically by the CLI) or
from the process environment.
Platform authentication
Normal initialization manages these automatically. Non-interactive setup
requires the complete access-token triple.
| Var | Purpose |
|---|---|
VERCEL_OIDC_TOKEN |
Interactive linked-project credential used by Gateway and Sandbox. Stored in .env.local by setup. |
VERCEL_TOKEN |
Non-interactive Vercel access token. |
VERCEL_TEAM_ID |
Non-interactive team paired with VERCEL_TOKEN. |
VERCEL_PROJECT_ID |
Non-interactive project paired with VERCEL_TOKEN. |
Model authentication
| Var | Used by | Purpose |
|---|---|---|
AI_GATEWAY_API_KEY |
Gateway route | Optional long-lived alternative to linked-project OIDC. Expanded for the selected agent. |
ANTHROPIC_AUTH_TOKEN |
process, revalidate, triage (Claude backend) |
API token for the Claude Agent SDK. AI Gateway-issued or Anthropic-issued. Set this if you don't use AI_GATEWAY_API_KEY. |
ANTHROPIC_BASE_URL |
same | Default (when AI_GATEWAY_API_KEY is set): https://ai-gateway.vercel.sh. Set to https://api.anthropic.com for direct Anthropic. |
<ai.apiKeyEnv> |
Direct/custom route | User-chosen variable containing the provider credential. The name is stored in config; the value comes from .env.local or the process. |
Optional
| Var | Used by | Purpose |
|---|---|---|
OPENAI_API_KEY |
--agent codex, --agent pi --model openai/... |
Codex SDK token or Pi OpenAI-provider token. Unset is fine if AI_GATEWAY_API_KEY is set. |
OPENAI_BASE_URL |
--agent codex |
Default (when AI_GATEWAY_API_KEY is set): https://ai-gateway.vercel.sh/v1. |
PI_CODING_AGENT_DIR |
--agent pi |
Optional Pi config/auth directory. Defaults to ~/.pi/agent; local non-sandbox runs can reuse auth.json there. |
DEEPSEC_AGENT_DEBUG |
both backends | Set to 1 to enable verbose agent logging. |
DEEPSEC_DATA_ROOT |
core | Override the data directory location. Equivalent to dataDir in config. |
Plugin-specific
Each plugin documents its own env vars in its README.
Project-config gating example
For a monorepo where most projects shouldn't get an organization plugin:
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.
File: docs/data-layout.md
title: "Data layout"
data/ is deepsec's on-disk state. Each project owns a subdirectory. Scan
and analysis records are append-only across runs; setup evidence is replaced
atomically when its inputs change.
data//
โโโ project.json # rootPath, githubUrl (auto-managed)
โโโ INFO.md # repo context injected into AI prompts
โโโ config.json # priorityPaths, promptAppend, ignorePaths (optional)
โโโ setup/
โ โโโ setup-state.json # resumable one-shot phase checkpoints
โ โโโ surface-inventory.json# generated structured ingress inventory
โโโ files/ # one JSON per scanned source file (FileRecord)
โ โโโ path/to/source.ts.json
โโโ runs/ # one JSON per run (RunMeta)
โ โโโ 20260429215021-19ac.json
โโโ reports/ # generated markdown + JSON reportsThe scaffold keeps INFO.md and SETUP.md trackable, while ignoringproject.json, setup/, files/, runs/, and reports/. The generatedsetup/ evidence may contain repository paths and platform project IDs, but
never credential values.
The schemas below are the source of truth for any tool that readsdata/ directly. They live inpackages/core/src/types.ts.
project.json โ `ProjectConfig`
Auto-written on first scan; safe to edit by hand.
| Field | Type | Purpose |
|---|---|---|
projectId |
string |
Matches the directory name. |
rootPath |
string |
Absolute path to the codebase. Updated each scan with the most recent --root. |
createdAt |
string (ISO) |
Project init time. |
githubUrl |
string? |
https://github.com/owner/repo/blob/branch โ used for clickable links in exports. Auto-detected from git remote if not set. |
config.json โ per-project overrides
Optional. Read by scan and the AI agents.
| Field | Type | Purpose |
|---|---|---|
priorityPaths |
string[] |
Path prefixes processed first. |
promptAppend |
string |
Free-form text appended to the system prompt for this project. |
ignorePaths |
string[] |
Glob patterns to skip during scan. |
INFO.md
Free-form markdown injected into the AI prompt for process,triage, and revalidate. See
getting-started for the automatic repository-analysis
phase and the manual scaffold fallback.
`setup/setup-state.json` โ SetupState
Versioned coordinator state for deepsec init / deepsec setup. It records:
- phase status and input/output digests;
- source, INFO, inventory, and generated-matcher fingerprints;
- baseline/final scan IDs and summaries;
- processing run ID;
- non-secret project-link/model-route verification metadata; and
- proposed matcher specs, accepted and rejected slugs, exact rejection
reasons, and whether an attempt returned no proposal.
Writes use a temporary file plus rename. A phase marked running or error
is safe to retry. A complete checkpoint is reused only when its input digest
matches and required outputs still exist.
`setup/surface-inventory.json`
The repository-analysis response used by coverage evaluation. Each surface
has a stable ID, kind (http, rpc, queue, cron, cli, webhook,agent-tool, or other), exposure, file globs, representative files,
optional anchor regexes, and expected auth primitives. Paths are relative to
the target root and validated before use.
`files/.json` โ FileRecord
The core per-file accumulator. Every stage adds to this record;
nothing is overwritten. Re-scanning merges new candidates.
Re-processing appends to analysisHistory. Revalidation annotates
findings rather than replacing them.
The on-disk path mirrors the source path under <rootPath> plus a.json suffix (src/api/auth.ts โ files/src/api/auth.ts.json).
Top-level fields
| Field | Type | Purpose |
|---|---|---|
filePath |
string |
Path relative to rootPath. |
projectId |
string |
The owning project. |
candidates |
CandidateMatch[] |
Regex matcher hits โ see below. |
lastScannedAt |
string (ISO) |
Most recent scan timestamp. |
lastScannedRunId |
string |
runId of the scan that last touched this file. |
fileHash |
string (sha-256) |
Source content hash at last scan. |
findings |
Finding[] |
Latest set of AI-produced findings. |
analysisHistory |
AnalysisEntry[] |
Append-only log of every AI investigation. |
gitInfo |
object? |
Git committer info + ownership data, written by enrich. |
status |
"pending" | "processing" | "analyzed" | "error" |
Lifecycle state. |
lockedByRunId |
string? |
When non-empty, a run holds this file. Cleared on completion. |
`CandidateMatch`
| Field | Type | Purpose |
|---|---|---|
vulnSlug |
string |
Matcher slug that fired. |
lineNumbers |
number[] |
1-indexed source lines. |
snippet |
string |
Short excerpt around the first match. |
matchedPattern |
string |
Human-readable label of the regex (the matcher's label). |
`Finding`
| Field | Type | Purpose |
|---|---|---|
severity |
"CRITICAL" | "HIGH" | "MEDIUM" | "HIGH_BUG" | "BUG" | "LOW" |
See README severity table. |
vulnSlug |
string |
Matcher slug or other-<topic> if no matcher fits. |
title |
string |
One-sentence summary. |
description |
string |
Full explanation. |
lineNumbers |
number[] |
1-indexed lines. |
recommendation |
string |
Suggested fix. |
confidence |
"high" | "medium" | "low" |
The agent's self-rated confidence. |
triage |
Triage? |
Set by triage. |
revalidation |
Revalidation? |
Set by revalidate. |
`Triage` (set by `deepsec triage`)
| Field | Type | Purpose |
|---|---|---|
priority |
"P0" | "P1" | "P2" | "skip" |
Recommended action bucket. |
exploitability |
"trivial" | "moderate" | "difficult" |
Effort to weaponize. |
impact |
"critical" | "high" | "medium" | "low" |
Blast radius if exploited. |
reasoning |
string |
Why this priority. |
triagedAt |
string (ISO) |
Timestamp. |
model |
string |
Model used for triage. |
`Revalidation` (set by `deepsec revalidate`)
| Field | Type | Purpose |
|---|---|---|
verdict |
"true-positive" | "false-positive" | "fixed" | "uncertain" |
Re-checked verdict. |
reasoning |
string |
Why this verdict. Includes git-history evidence if fixed. |
adjustedSeverity |
Severity? |
Set if revalidation re-rates the finding. |
revalidatedAt |
string (ISO) |
Timestamp. |
runId |
string |
runId of the revalidate run. |
model |
string |
Model used. |
`AnalysisEntry`
One per AI investigation of this file. Append-only โ nothing is ever
deleted.
| Field | Type | Purpose |
|---|---|---|
runId |
string |
The producing run. |
investigatedAt |
string (ISO) |
Timestamp. |
durationMs |
number |
Wall-clock total. |
durationApiMs |
number? |
API time only (excludes process orchestration). |
agentType |
string |
claude-agent-sdk or codex. |
model |
string |
Model identifier. |
modelConfig |
Record<string, unknown> |
Provider-specific settings echoed back. |
agentSessionId |
string? |
The agent's session/thread id, for reproducing or replaying. |
findingCount |
number |
Findings produced in this entry. |
numTurns |
number? |
Conversation turn count. |
costUsd |
number? |
Estimated USD cost. |
usage |
{ inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens }? |
Token accounting. |
refusal |
RefusalReport? |
See models. |
codexStderr |
string? |
Captured codex stderr when an investigation produced 0 output tokens (forensic only). |
reinvestigateMarker |
number? |
Wave marker from --reinvestigate <N>. |
`RefusalReport`
| Field | Type | Purpose |
|---|---|---|
refused |
boolean |
True if the agent skipped or declined any part of the investigation. |
reason |
string? |
Free-form reason if refused. |
skipped |
Array<{ filePath?: string; reason: string }>? |
Per-file skip reasons. |
raw |
string? |
Trimmed raw model response to the follow-up question, for debugging. |
`gitInfo` (set by `deepsec enrich`)
| Field | Type | Purpose |
|---|---|---|
recentCommitters |
Array<{ name, email, date }> |
Top contributors over the file's recent history. |
enrichedAt |
string (ISO) |
Last enrich timestamp. |
ownership |
OwnershipData? |
If an ownership plugin is active, structured ownership/escalation data. |
`status` lifecycle
pending -- scan finished, awaits AI
โ
processing -- a run is currently investigating (lockedByRunId set)
โ
analyzed -- AnalysisEntry appended; findings updatederror is set if the agent crashed mid-investigation. Re-runningprocess will retry error and pending files.
`runs/.json` โ RunMeta
One per scan / process / revalidate invocation. Used for status
reporting (deepsec status) and for filtering exports by run.
| Field | Type | Purpose |
|---|---|---|
runId |
string |
<YYYYMMDDHHMMSS>-<rand4>. Sortable. |
projectId |
string |
Owning project. |
rootPath |
string |
Resolved root for the run. |
createdAt |
string (ISO) |
Run start. |
completedAt |
string? (ISO) |
Run end (absent while running). |
type |
"scan" | "process" | "revalidate" |
Stage. |
phase |
"running" | "done" | "error" |
Terminal status. |
scannerConfig |
{ matcherSlugs }? |
Set on scan runs. |
processorConfig |
{ agentType, model, modelConfig }? |
Set on process / revalidate runs. |
stats |
object |
Counters: filesScanned, candidatesFound, findingsCount, totalCostUsd, truePositives, falsePositives, โฆ |
reports/
Generated by deepsec report. One markdown per project plus a JSON
summary. Re-running report overwrites; nothing here is incremental.
Reading data/ directly
The append-only model means a few patterns work well:
- Find every TP HIGH+ finding across a project:bash
jq -r '. as $r | $r.findings[] | select(.revalidation.verdict=="true-positive") | select(.severity=="HIGH" or .severity=="CRITICAL") | [$r.filePath, .severity, .title] | @tsv' data//files/**/*.json - Total spend on a project:bash
jq -s 'map(.analysisHistory[].costUsd // 0) | add' data//files/**/*.json - Files still pending after a run:bash
jq -r 'select(.status=="pending") | .filePath' data//files/**/*.json
For richer queries, prefer deepsec export --format json โ it applies
filters consistently with the rest of the CLI.
File: docs/faq.md
title: "FAQ"
How should I install deepsec?
deepsec lives in a .deepsec/ directory at the root of the repo you
want to scan, checked into git so teammates inherit project context.
From the codebase's repo root:
npx deepsec initThis installs the isolated workspace, links and verifies Vercel Sandbox,
creates the project threat model, checks scan coverage, generates safe
project-specific matchers when needed, and processes candidates. Re-run it
to resume, or use pnpm deepsec setup from inside .deepsec/.
Use npx deepsec init --scaffold-only if you only want the files,--model-auth direct with --ai-api-key-env to use your own model key,
or --model-auth local to rely on machine-wide claude/codex logins
without any API key. See
getting-started for details on both.
.deepsec/ has its own package.json and node_modules/ โ separate
from the parent repo's lockfile and tooling. The parent repo only
needs to know .deepsec/ exists.
To scan another codebase from the same .deepsec/, runpnpm deepsec init-project . Each project gets its owndata// subdirectory.
What about non-JS codebases?
deepsec is polyglot (TS, Go, Python, Lua, Terraform, โฆ). The parent
repo doesn't need to be a Node project โ .deepsec/ is self-contained
and only needs pnpm or npm inside that one directory.
`.gitignore` policy
The scaffold keeps INFO.md, SETUP.md, deepsec.config.ts, andgenerated-matchers.ts trackable. It ignores credentials/project-link files
and reproducible state (.env.local, .vercel/, data/*/setup/,data/*/files/, data/*/runs/, and reports).
How much does it cost?
The expensive stage is process. When explicitly using Claude Opus with
typical settings
(--concurrency 5 --batch-size 5):
| Files | Approx cost | Approx wall time |
|---|---|---|
| 100 | $25โ60 | 5โ15 min |
| 500 | $130โ300 | 25โ60 min |
| 2,000 | $500โ1200 | 1.5โ4 hr |
Costs swing 2โ3x based on file complexity. Run --limit 50 first to
calibrate before committing to a full pass.
triage is ~1ยข/finding. revalidate is comparable to process.
Should I use Claude or Codex?
Both work. Different strengths:
- Codex (gpt-5.5): the default; read-only/no-network repository tools
and strong grep-heavy investigation. - Claude (Opus): strong at authorization shapes and cross-file flows;
typically more expensive.
Mix them. Run Claude first, then re-process unconvincing findings with--agent codex --reinvestigate for a second opinion. Findings dedupe
across agents.
Should I use Gateway or my own provider key?
Either works. The project link is always created because it also authorizes
Sandbox; the model route is independent. Gateway is the default and uses
linked-project OIDC. A direct route names your own OpenAI/Anthropic variable,
and a custom Pi route can declare an HTTPS endpoint and auth header.
MY_ANTHROPIC_KEY=... npx deepsec init \
--agent claude --model-auth direct \
--ai-provider anthropic --ai-api-key-env MY_ANTHROPIC_KEYSee vercel-setup for route persistence, headless
project credentials, and host-side Sandbox credential brokering.
How accurate is it? What's the FP rate?
After revalidation: ~10โ29% on HIGH+.
Two things help most:
- Revalidate
HIGH+before acting on findings. Worth the cost. - Review setup's
INFO.mdand surface inventory. Correct auth primitives
and representative files improve both prompts and coverage decisions.
When should I use sandbox mode?
deepsec sandbox process fans work across Vercel Sandbox microVMs
in parallel. Worth it when:
- The repo is large enough that local concurrency saturates your laptop.
- You want results in under an hour on a 5k+ file repo.
- You're running this as a scheduled job in CI/CD.
Otherwise local execution is simpler. Normal initialization already creates
the exact project link and keeps Sandbox-capable credentials in scope, so
switching later requires no additional onboarding. It does not create a
billable Sandbox until you explicitly run a Sandbox command.
What happens to my code? Is it sent anywhere?
The AI agents read source code from your local repo and send relevant
snippets to the configured LLM provider as part of investigation
prompts. With Vercel AI Gateway, the gateway has zero data retention;
prompts aren't stored. With direct Anthropic, see Anthropic's data
retention policy.
deepsec itself doesn't phone home or report telemetry. The data//
directory stays on your machine unless you explicitly export it.
Can I run this in CI?
Yes. The natural shape:
# Cron โ full scan every Sunday
pnpm deepsec scan --project-id main --root .
pnpm deepsec process --project-id main --concurrency 5
pnpm deepsec revalidate --project-id main --min-severity HIGH
pnpm deepsec export --project-id main --format json --out findings.json
# Per-PR โ incremental scan on changed files only
pnpm deepsec scan --project-id main --root .
pnpm deepsec process --project-id main --filter $CHANGED_PATH_PREFIXThe data/ directory is your state โ persist it between CI runs (cache
it as a build artifact) or just re-scan from scratch each time.
Is it incremental?
Yes:
scanmerges new candidates into existing FileRecords; doesn't
re-investigate already-analyzed files.processonly touches files withstatus: "pending", unless you
pass--reinvestigate(re-investigate everything) or--reinvestigate <N>(re-investigate, tagged with wave marker N โ a
later run with the same N skips files already processed in this wave).revalidateonly touches findings without arevalidationfield
unless--forceis set.
What if a run errors out partway through?
Just re-run the same command. process and revalidate are safe to
re-run โ files that already finished are kept (no double billing), and
only files that didn't finish get picked up. Same is true after a
Ctrl-C, a network blip, a transient model error, or a quota stop. No
state to clean up; no flag to set.
If you specifically want to redo work that already succeeded, that's
what --reinvestigate (process) and --force (revalidate) are for.
How do I add a matcher for my codebase?
See writing-matchers. Short version: hand
your .deepsec/data/ and the target repo to your coding agent with the
prompt in that doc โ it'll spot entry-point coverage gaps the default
matchers miss and write matchers tailored to your codebase.
What if my codebase is in a language deepsec doesn't have matchers for?
The AI processor is language-agnostic and will investigate any
text-readable source file. The thinner the regex layer, the more the
process stage carries. A few starter matchers for the new language are
worth writing; they front-load file selection so the AI gets the most
promising sites first.
What if I find a vulnerability in deepsec itself?
See SECURITY.md. Don't open a public issue โ use
GitHub Security Advisories instead.
File: docs/getting-started.md
title: "Getting started"
Run your first scan
From the root of the repository you want to scan:
npx deepsec initThe command asks you two things, then does the rest on its own:
- Which model to use. You'll see a short list of recommended
model and reasoning-level combinations, each with its score on the
DeepSecBench benchmark and its cost relative to the cheapest option.
Pick one, or choose the last option to type in any model of your own. - How to pay for model calls. The default routes through Vercel AI
Gateway โ deepsec logs you in to Vercel if needed and sets up a small
dedicated project to hold credentials (nothing billable is created
during setup). If you'd rather use your own OpenAI or Anthropic API
key, no Vercel account is needed โ see
Using your own API key.
After that, deepsec works unattended. It creates a .deepsec/ folder next
to your code (this is the only thing it adds to your repository), studies
the codebase to understand what it does and where the risky areas are,
runs a fast pattern scan, and then starts the AI review of the flagged
files. Depending on repository size, the AI review can take from minutes
to many hours.
If it stops partway
Just run the same command again:
npx deepsec initDeepsec remembers how far it got. Finished steps are skipped and the run
continues where it left off. This is also how you resume after hitting a
cost limit, losing your connection, or pressing Ctrl-C.
If setup cannot cover an inventoried surface, it stops before paid AI
processing and prints the exact inventory, state, generated-matcher, and
resume commands. The state records matcher proposals and rejection reasons.
Re-running setup makes up to two fresh matcher-repair attempts, so a coverage
pause is recoverable after editing generated-matchers.ts or the underlying
repository pattern.
Limiting time and cost
You can bound a run and pick up the rest later:
npx deepsec init --max-cost-usd 100 --max-duration 2hWhen a limit is reached, deepsec stops at a safe point. Run the same
command again to continue. Durations need an explicit unit: ms, s,m, or h.
If the run stops because your model provider ran out of credits, deepsec
tells you where to top up; re-run the same command afterwards.
Reading your results
Everything deepsec produces lives in the .deepsec/ folder. To get a
readable report:
cd .deepsec
pnpm deepsec export --format md-dir --out ./findingsThat writes one markdown file per finding into ./findings. For a quick
overview instead:
pnpm deepsec reportEveryday commands
After the first run, work from inside .deepsec/:
pnpm deepsec status # where things stand
pnpm deepsec scan # re-run the fast pattern scan (free, no AI)
pnpm deepsec process # AI review of new or changed candidates
pnpm deepsec revalidate # re-check findings; cuts false positives
pnpm deepsec export --format md-dir --out ./findingsscan costs nothing (it's local pattern matching). process is the
expensive AI stage. All of these resume cleanly if interrupted, and
re-running them only looks at work that isn't done yet.
Using your own API key
You don't need a Vercel account to use deepsec with your own API key.
To use your own OpenAI key:
MY_OPENAI_KEY=... npx deepsec init \
--agent codex \
--model-auth direct \
--ai-provider openai \
--ai-api-key-env MY_OPENAI_KEYFor Anthropic, use --agent claude --ai-provider anthropic. Deepsec
stores only the name of the environment variable, never the key itself.
Export the variable again for later commands, or put it in.deepsec/.env.local. See vercel-setup for other
providers and the full credential reference.
Using local subscriptions
If the claude or codex CLI is already logged in on your machine, you
can skip model credentials entirely. Pick Use local subscriptions at
the interactive model-access prompt, or pass the flag directly:
npx deepsec init --model-auth localDeepsec then configures nothing for model access: no API key, no gateway
token, no environment variables. Later commands (process,revalidate, triage) skip their credential checks and rely on the
machine-wide login โ if it's missing, the agent SDK itself reports a
clear error on first use.
Two caveats: the login must exist for the harness you actually run
(claude for --agent claude, a logged-in codex for --agent codex),
and sandbox commands still need a real API token
(AI_GATEWAY_API_KEY), because a machine-local login can't be brokered
into an isolated sandbox.
Scaffold only
To create the workspace files without running any of the setup:
npx deepsec init --scaffold-onlyThis writes the .deepsec/ skeleton โ deepsec.config.ts,package.json, README.md, AGENTS.md, .gitignore, anddata//{INFO.md,SETUP.md,project.json} for the first project โ
and stops. It does not install dependencies, log in to Vercel, pick a
model, scan, or start AI processing.
Use it when you want to review or commit the scaffold before anything
runs, drive the remaining setup from a coding agent, or do the steps by
hand. The command prints the manual path: pnpm install inside.deepsec/, an agent prompt for filling in INFO.md, thenpnpm deepsec scan and pnpm deepsec process. You can also runpnpm deepsec setup (or re-run npx deepsec init) later to complete
the normal automated flow from where the scaffold left off.
Two files worth a look
Setup writes two things you may want to review:
data//INFO.mdโ a short description of your codebase and its
security-relevant areas that gets injected into every AI investigation.
It's meant to be hand-edited: the more accurate it is, the better the
findings..deepsec/generated-matchers.tsโ extra scan patterns deepsec generated
to cover gaps in your codebase. Review and commit this file. To go
further, see writing-matchers.
A note on trust
Treat deepsec like a coding agent: during setup it reads your source code
with an AI agent that authenticates to your model provider. Only run it on
code you trust at that level. For scanning untrusted pull requests, use
the guarded CI patterns in reviewing-changes,
or run the work in isolated cloud sandboxes (this part does use a Vercel
account, since the sandboxes run on Vercel):
pnpm deepsec sandbox process --project-id my-app --sandboxes 10 --concurrency 4Sandboxes never see your real model credentials. The host machine keeps
them and injects them only at the model provider's servers.
Running from CI or an agent
Without a terminal attached, deepsec automatically runs in headless mode:
it never prompts or opens a browser. The two commands that matter:
# Preview everything setup would do, without changing anything
npx deepsec init --plan --output json
# Run unattended: accept defaults, pick a model by profile, stream events
npx deepsec init --yes --model-profile value --output jsonl--model-profile picks the model for you: best (highest benchmark
score), value (best score at reasonable cost), or budget (cheapest).
Exit code 2 means deepsec needs input it couldn't get headlessly (for
example a Vercel login) โ the JSON output says exactly what to do and how
to resume. Exit code 3 means a cost or duration limit was reached; re-run
the same command to continue. In CI, a gateway-routed setup can
authenticate with explicit values instead of a login:
VERCEL_TOKEN=... VERCEL_TEAM_ID=team_... VERCEL_PROJECT_ID=prj_... \
npx deepsec init --headlessCoding agents should read the docs deepsec installs into the workspace,
which always match the installed version: start with.deepsec/node_modules/deepsec/SKILL.md, then the topics under.deepsec/node_modules/deepsec/dist/docs/.
Scanning more than one repository
A single .deepsec/ workspace can track several repositories. From the
existing workspace:
pnpm deepsec init-project ../another-service --id another-service
pnpm deepsec setup --project-id another-serviceWhen a workspace has more than one project, pass --project-id to the
everyday commands.
Next
- configuration โ project, model route, and
environment configuration. - writing-matchers โ improving scan coverage
with your own matchers. - vercel-setup โ credentials, CI setups, and
troubleshooting the Vercel connection. - architecture โ how the pipeline works internally.
File: docs/models.md
title: "Models"
deepsec talks to LLMs through interchangeable agent backends:
| Backend | Default model | Used by |
|---|---|---|
codex (default) |
gpt-5.5 |
process, revalidate |
claude |
claude-opus-4-8 |
process, revalidate |
pi |
zai/glm-5.2 |
process, revalidate |
claude (triage) |
claude-sonnet-4-6 |
triage (Claude-only) |
Interactive one-shot setup recommends five benchmark-backed combinations:
GPT-5.6 Sol, Claude Opus 5, Kimi K3, Grok 4.5, and the current DeepSeek entry.
Deepsec fetches the latest score, reasoning level, harness, and total run cost
from DeepSecBench,
then displays cost relative to the cheapest recommendation. A bundled snapshot
keeps onboarding usable offline and is visibly marked as cached. You can also
paste any custom model slug.
Choose the backend/model non-interactively so repository analysis and the
first processing pass use the same pair:
npx deepsec init --agent codex --model gpt-5.5For benchmark-backed headless selection, use a profile:
| Profile | Selection rule |
|---|---|
best |
Highest compatible DeepSecBench score |
value |
Highest score whose run cost is at most 2.5ร the cheapest recommendation |
budget |
Cheapest compatible recommended combination |
npx deepsec init --yes --model-profile value --output jsonlDirect OpenAI and Anthropic credentials automatically restrict profiles to a
compatible Codex or Claude harness; custom routes restrict them to Pi.
The built-in backends work with Vercel AI Gateway through the linked
workspace's OIDC credential. The model credential route is independent of the
Vercel/Sandbox project link and is persisted as non-secret ai config. Direct
OpenAI/Anthropic and custom Pi routes are documented in
vercel-setup.
There is also a local route (--model-auth local, or "Use local
subscriptions" in the interactive prompt) for machines where the claude orcodex CLI is already logged in. It configures no credential at all and
disables the env-var preflight checks โ the machine-wide login is used
directly. Sandbox commands are the exception: they must broker a real token,
so they still require AI_GATEWAY_API_KEY (or an equivalent key).
CLI selection
# Codex (default backend), default model:
pnpm deepsec process --project-id my-app
# Claude with a specific model:
pnpm deepsec process --project-id my-app --agent claude --model claude-sonnet-4-6
# Codex backend, default model:
pnpm deepsec process --project-id my-app --agent codex
# Codex backend, specific model:
pnpm deepsec process --project-id my-app --agent codex --model gpt-5.4
# Pi backend through Vercel AI Gateway, default model:
pnpm deepsec process --project-id my-app --agent pi
# Pi with an AI SDK / AI Gateway style model id:
pnpm deepsec process --project-id my-app --agent pi --model zai/glm-5.2
# Triage uses Claude; pass a cheaper model if you want:
pnpm deepsec triage --project-id my-app --model claude-haiku-4-5--agent, --model, and --thinking-level are also accepted on setup andrevalidate. Setup persists the interactive choice as defaultAgent,defaultModel, and defaultThinkingLevel, checkpoints the exact combination,
and invalidates affected phases when it changes.
Thinking level
process and revalidate accept --thinking-level to control how much
reasoning effort the agent spends per batch:
pnpm deepsec process --project-id my-app --thinking-level highAccepted values: minimal, low, medium, high, xhigh. The
default is xhigh. Deepsec optimizes for finding hard bugs, not for
cost. Dial down for cheaper reinvestigation waves or quick smoke runs
over large repos.
The flag maps onto each backend's native dial:
| Backend | Setting |
|---|---|
codex |
model reasoning effort (minimalโxhigh) |
pi |
thinking level (minimalโxhigh) |
claude |
adaptive-thinking effort (minimal โ low, xhigh โ max) |
It applies to the main investigation/revalidation runs only.
Special-purpose follow-up calls (the refusal report, JSON repair) keep
their own fixed, cheap settings regardless of the flag.
Like other subcommand flags, it passes through sandbox mode unchanged:
pnpm deepsec sandbox process --project-id my-app --sandboxes 30 --thinking-level highWhy these defaults
`claude-opus-4-8` for `process` and `revalidate`
Investigating a candidate site is a multi-step reasoning task: trace
control flow, recognize an auth boundary, decide whether input is
attacker-controlled, judge severity. Stronger reasoning models pay for
themselves in lower FP rate, even at higher per-call cost. Opus is the
strongest of the Claude family at this kind of code reasoning.
If cost matters more than precision (a 10k-file repo, a quick triaged
starter list), drop to claude-sonnet-4-6. Same prompt, ~3ร cheaper,
~10โ20% higher FP rate.
`gpt-5.5` for the Codex backend
Codex is the OpenAI-flavored agent loop: grep-heavy, fast, runs in a
strict read-only sandbox. gpt-5.5 is the right balance of reasoning
and cost for that loop. gpt-5.5-pro is the most careful Codex
option at significantly higher cost; gpt-5.4 and below are fine for
follow-up reinvestigation passes.
Pi for alternate harness runs
Pi uses @earendil-works/pi-coding-agent with read-only tools
(read, grep, find, ls) and the same deepsec prompt/schema as the
other backends. Its default model is GLM 5.2 through Vercel AI Gateway:
AI_GATEWAY_API_KEY=vck_...
pnpm deepsec process --project-id my-app --agent piNormal setup pulls and uses the exact linked workspace's OIDC credential.
For OpenAI-compatible gateways such as Martian, select and persist a custom
route during setup:
MARTIAN_API_KEY=...
pnpm deepsec setup --project-id my-app \
--agent pi \
--model openai/gpt-5.5 \
--model-auth custom \
--ai-provider martian \
--ai-base-url https://api.withmartian.com/v1 \
--ai-api-key-env MARTIAN_API_KEY \
--ai-credential-header authorization:bearerLater process, revalidate, and Sandbox commands resolve the persisted
route. Per-command --ai-provider, --ai-base-url, --ai-api-key-env, and
repeatable --ai-header name=value remain available as Pi runtime overrides.
`claude-sonnet-4-6` for `triage`
Triage buckets findings into P0/P1/P2/skip without re-reading the code.
It just looks at the finding text. That's a cheap task; Opus is
overkill. Sonnet keeps triage at ~1ยข/finding.
Refusals
Models occasionally refuse to investigate a candidate โ usually when the
source contains an exploit pattern they read as harmful, or when a path
trips a content filter. After every batch, deepsec issues a follow-up
turn asking the agent whether it skipped or declined anything:
Looking back at the investigation: was there anything you declined
to fully analyze, refused to look at, or skipped because the content
or the task felt uncomfortable or out of scope?
The agent answers in a structured JSON shape (see parseRefusalReport
in packages/processor/src/agents/shared.ts). If refused: true, the
batch gets a refusal record in run metadata, the per-batch log line
shows a โ ๏ธ refusal marker, and the refusal field on the FileRecord
sticks around for audit. No silent skips.
Claude Opus and gpt-5.5 refuse less than 1% of batches in practice. A
refused batch produces no false negatives โ affected files staypending (revalidation keeps the original verdict), so re-running--reinvestigate against the other backend picks up the dropped sites.
Findings dedupe across agents, so you don't pay twice.
If a single file consistently triggers a refusal (>5% of batches), it's
usually one path with a hard-to-disambiguate exploit pattern. Add it toconfig.json:ignorePaths, or run that file alone with --batch-size 1
so the refusal doesn't take a batch of otherwise-fine files down with
it.
Future models (e.g. Anthropic Mythos)
The model is a flag, not a baked-in choice. When a stronger reasoning
model lands โ Anthropic's Mythos, a next-tier OpenAI release, an
open-weight contender โ point --model at the new identifier and the
rest of deepsec stays unchanged:
pnpm deepsec process --project-id my-app --model anthropic-mythos-1
pnpm deepsec process --project-id my-app --agent codex --model gpt-6
pnpm deepsec process --project-id my-app --agent pi --model vercel-ai-gateway/openai/gpt-6Two small integration points:
- The model identifier โ whatever string the provider's SDK
accepts. deepsec passes it through unchanged. No code change needed
to use a new model on either backend. - Pricing for the cost-per-batch readout. The Claude Agent SDK
reports cost natively, so new Claude-family models drop in with
zero code changes. Codex doesn't, so add a line toMODEL_PRICING_USD_PER_M_TOKENSinpackages/processor/src/agents/codex-sdk.tsfor each new
OpenAI/Codex model. Without it, the batch still runs โ the cost
readout is simply omitted.
When a new model becomes the right default, change the relevant entry
in packages/deepsec/src/agent-defaults.ts (one string per backend) and
the DEFAULT_MODEL constant in the corresponding agent file. Existing
data and findings are unaffected โ deepsec records which agent + model
produced each finding, so a model change shows up cleanly in theanalysisHistory of any re-investigated file.
A useful pattern when a new model lands: re-run process with--reinvestigate <N> (a wave marker) against the existing
high-severity findings to see whether the new model overturns
verdicts. The wave marker tags the new analysis without losing the
old one.
File: docs/plugins.md
title: "Plugins"
A deepsec plugin can fill any of five 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 |
A single plugin can fill any subset.
The plugin contract lives inpackages/core/src/plugin.ts:
export interface DeepsecPlugin {
name: string;
matchers?: MatcherPlugin[];
notifiers?: NotifierPlugin[];
ownership?: OwnershipProvider;
people?: PeopleProvider;
executor?: ExecutorProvider;
agents?: AgentPluginRef[];
commands?: (program: unknown) => void; // commander program
}Plugins are loaded from deepsec.config.ts:
import { defineConfig } from "deepsec/config";
import myPlugin from "@my-org/deepsec-plugin";
export default defineConfig({
projects: [{ id: "my-app", root: "../my-app" }],
plugins: [myPlugin({ /* options */ })],
});Where to put your plugin
For an org-internal plugin: a workspace package inside this repo, or a
sibling repo. Either works; pnpm/npm workspaces handle the resolution.
For a shared plugin: publish to npm under your scope.
Naming convention: @<scope>/plugin- (Vite style),
e.g. @my-org/plugin-internal-services.
Slot 1: matchers
Most common. Same shape as a built-in matcher; see
writing-matchers for how to write one.
// my-plugin/src/matchers/internal-rpc.ts
import type { MatcherPlugin, CandidateMatch } from "deepsec/config";
import { regexMatcher } from "deepsec/config";
export const internalRpcMatcher: MatcherPlugin = {
slug: "internal-rpc-no-auth",
noiseTier: "precise",
filePatterns: ["**/*.go"],
match(content, filePath) {
return regexMatcher("internal-rpc-no-auth", [
{ regex: /NewMyServiceHandler\s*\([^)]*\)/, label: "service handler" },
], content);
},
};// my-plugin/src/index.ts
import type { DeepsecPlugin } from "deepsec/config";
import { internalRpcMatcher } from "./matchers/internal-rpc.js";
export default function myPlugin(): DeepsecPlugin {
return {
name: "@my-org/plugin-internal-services",
matchers: [internalRpcMatcher],
};
}Activate it:
// deepsec.config.ts
import myPlugin from "@my-org/plugin-internal-services";
export default defineConfig({
projects: [/* โฆ */],
plugins: [myPlugin()],
});The plugin's matchers are registered alongside deepsec's built-ins. Slugs
are unique. If your slug collides with a built-in, the plugin wins
(last-registered overrides). Useful for swapping a built-in matcher for
a tighter org-specific version.
A complete inline-plugin example with two real matchers lives atsamples/webapp/deepsec.config.ts andsamples/webapp/matchers/ โ the same
shape as a published plugin, just defined in the user's config file.
Slot 2: ownership
ownership maps a file to the team or person that owns it. deepsec enrich attaches this data to findings. Useful for routing notifications
and prioritizing review.
The contract:
interface OwnershipProvider {
name: string;
fetchOwnership(args: { filePath: string; repo: string }): Promise<OwnershipData | null>;
}OwnershipData covers contributors, escalation teams, manager email,
on-call info. See packages/core/src/types.ts:OwnershipData.
Return null when ownership data is unavailable; callers treat that as
a soft-fail.
A minimal ownership provider that reads from a CODEOWNERS file:
import type { OwnershipProvider } from "deepsec/config";
import fs from "node:fs";
export function codeownersProvider(rootPath: string): OwnershipProvider {
return {
name: "codeowners",
async fetchOwnership({ filePath }) {
// Parse CODEOWNERS, match filePath against globs, return the
// first matching team/email.
// Return null if no match or file doesn't exist.
// ...
},
};
}An external organization plugin can wrap an internal directory or
ownership oracle the same way.
Slot 3: people
people looks up a person by email or name and returns their metadata
(manager, slack handle, github username). Used by ownership and by
notifiers for @-mentions and escalation.
interface PeopleProvider {
name: string;
lookup(query: string): Promise;
lookupManager?(person: Person): Promise;
}Person has a generic core (name, email, title, managerKey) plus
an extra map for provider-specific fields (e.g. slackId, slackHandle).
An external organization plugin can wrap an internal people directory
the same way.
Slot 4: notifiers
notifiers are where findings get reported. Slack, GitHub Issues,
webhooks, an internal incident system; whatever fits.
interface NotifierPlugin {
name: string;
notify(params: NotifyParams): Promise<FindingNotification>;
}NotifyParams carries the finding, the FileRecord, and the projectId.FindingNotification carries an externalId and externalUrl for
correlation back to the source.
deepsec doesn't ship a notifier in core. The original Slack notifier was
removed during open-sourcing because Slack belongs in a plugin. A
GitHub Issues notifier would be a good first plugin to write.
Slot 5: executor
executor runs deepsec commands on remote infrastructure. The in-tree@vercel/sandbox executor is the canonical example. Docker, Kubernetes,
and AWS-Batch executors all fit here.
interface ExecutorProvider {
name: string;
launch(req: ExecutorLaunchRequest, onLog: (m: string) => void): Promise<string>; // runId
collect(runId: string): Promise<void>;
status?(runId: string): Promise<ExecutorStatus>;
}The Vercel-Sandbox path lives inpackages/deepsec/src/sandbox/; it's
not yet routed through ExecutorProvider. That refactor is on the
roadmap. For now, this is the most experimental slot of the five.
Testing your plugin
Drop-in pattern:
// my-plugin/src/__tests__/plugin.test.ts
import { describe, expect, it } from "vitest";
import { createDefaultRegistry } from "deepsec/config";
import myPlugin from "../index.js";
describe("@my-org/plugin-internal-services", () => {
it("contributes the expected matchers", () => {
const plugin = myPlugin();
const slugs = plugin.matchers!.map(m => m.slug);
expect(slugs).toContain("internal-rpc-no-auth");
});
it("does not collide with built-ins", () => {
const built = new Set(createDefaultRegistry().slugs());
const plugin = myPlugin();
for (const m of plugin.matchers ?? []) {
// Either the slug is unique, or you're intentionally overriding.
// Document the overrides loudly.
}
});
});Resolution order
ownership, people, and executor are single-slot. The last
plugin to declare each wins. So a generic codeowners ownership plugin
can load first, and an org-specific oracle later in the plugins: [...]
array overrides it.
matchers, notifiers, and agents are additive. All plugin
contributions stack.
File: docs/reviewing-changes.md
title: "Reviewing changes (PR mode)"
deepsec process has a direct-invocation mode for reviewing a specific
set of files โ typically the files changed in a pull request. This is
the right tool when you want a fast, scoped read of changed code in CI,
rather than a whole-repo audit.
deepsec process --diff origin/mainHow it differs from a full scan
The standard flow is scan โ process over the entire repo:
| Step | What it looks at | What it produces |
|---|---|---|
scan |
The full source tree | Regex candidates per file |
process |
All pending candidates | AI findings on every flagged file |
Direct mode collapses both steps into one invocation, scoped to a file
list:
| Step | What it looks at | What it produces |
|---|---|---|
| Resolve files | --diff / --files / stdin |
A POSIX-relative file list under rootPath |
| Scoped scan | Only the listed files | Candidates as signals for the prompt (best-effort) |
| Always-process | The same listed files | AI findings โ even on files no matcher hit |
The scoped scan still runs because regex hits are useful prompt anchors
for the agent. Files with no hits still get a record and still get
investigated as a holistic review โ no signals, no scanner anchoring,
just the agent reading the file.
Flags
All five sources are mutually exclusive:
--diff <ref|range> Investigate `git diff --name-only <ref>` (e.g. origin/main, HEAD~1..HEAD)
--diff-staged Investigate the index vs HEAD
--diff-working Investigate uncommitted + untracked files
--files <csv> Investigate this comma-separated path list
--files-from Read newline-delimited paths from (or "-" for stdin)Other knobs:
--no-ignore Bypass the default ignore filter (test files, dist/, node_modules/, โฆ)
--comment-out Write a PR-comment-shaped markdown summary to (only when findings exist)
--project-id Override the project id (auto-derived from rootPath basename otherwise)
--root Override the project root (defaults to cwd or deepsec.config.ts)The usual --agent, --model, --concurrency, --batch-size,--max-turns, --thinking-level flags work the same as in standard
mode.
Auto-created projects
You don't need to run deepsec init first. When invoked with one of the
direct-mode flags, process will:
- Use
--project-idif you pass one. If it's already declared indeepsec.config.ts, the declared root is used; otherwise--root
(or the current working directory) is used. - Otherwise, derive the id from the basename of the resolved root.
- Write
data//project.jsonif it doesn't already exist.
Auto-creation is one-line and non-destructive โ it never modifies yourdeepsec.config.ts. It just ensures data// exists so file
records, run metadata, and the optional PR-comment markdown have
somewhere to land.
Exit codes
| Code | Meaning |
|---|---|
0 |
No findings produced in this run |
1 |
At least one finding was produced |
โ 1 |
Runtime error (bad input, missing credentials, โฆ) |
This makes direct mode a drop-in CI gate: the job fails when the agent
finds something. Net-new findings only count toward the exit code โ
re-running on a file with existing findings doesn't fail the build
unless something new is surfaced. Pre-existing findings (from a prior
full scan, or earlier PR review runs) on touched files are intentionally
excluded so the gate matches the change-scoped review model.
PR comments
--comment-out writes a markdown body summarizing the net-new
findings from this run โ same scope as the exit-code gate. Findings
already on touched files (from earlier full scans or prior PR reviews)
aren't re-surfaced. Descriptions and recommendations are truncated
(600 / 400 chars) so a multi-finding PR doesn't blow past GitHub's
65 KiB comment limit; the full text stays in data//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 onif: hashFiles('comment.md') != ''.
This is the workflow we use to review our own PRs โ copy it as-is:
/* Detailed source-code truncated for AI context efficiency. */How it works
- Two-job split.
analyzeruns PR-controlled code (the
user'spnpm install, their config, their source) with the AI
gateway secret in scope but no write permissions on the repo.commenthaspull-requests: writebut never runs any PR code โ
it consumes only the sanitizedcomment.mdartifact. A malicious
PR can't combine "execute arbitrary code" with "write to the
repository" in a single privileged step. - Same-repo-only gate.
if: github.event.pull_request.head.repo.full_name == github.repository
skips fork PRs entirely. Forks already don't receive repo secrets
underpull_request, so the deepsec step would just fail on
missing credentials anyway โ this gate is purely a UX cleanup
(fork PRs show "skipped" instead of red โ from a doomed run). fetch-depth: 0โ needed sogit diff origin/can
resolve against the merge base; the default shallow clone doesn't
have it.npm install @anthropic-ai/claude-codeโ the Claude Code CLI
is what the SDK actually drives. Installing it globally + settingCLAUDE_CODE_EXECUTABLE: claudeskips the SDK's bundled-binary
resolution, which can fail on Linux under some package managers.pnpm deepsecโ swap fornpx -y deepsec,npm exec deepsec,
oryarn deepsecto match your package manager.comment.mdis uploaded only when findings exist โ--comment-outwrites nothing on a green run, so the upload step'shashFilescheck skips and thecommentjob downloads no
artifact. That keeps the post-comment job a no-op when there's
nothing to say.
Threat model notes
- Don't grant
pull-requests: writeto a job that runs PR code.
The two-job pattern above keeps PR code in the no-writeanalyze
job. If you're tempted to collapse them, remember that a PR can
add arbitrary code to its ownpackage.jsonpostinstall scripts
or to a project config file that the CLI loads โ both run before
any of your own steps. - Pin actions to full SHAs in production. This example uses
major-version tags (@v4) for readability. For a hardened
deployment, swap each tag for the action's full commit SHA so a
compromised tag can't pivot into your secret-bearing job. See
GitHub's hardening guide. - The AI gateway secret still flows through PR code. Even with
the job split,analyzehas the secret in env while running
PR-controlledpnpm install. Theauthor_associationgate is
what prevents that from being a vulnerability. If you want
defense-in-depth, runanalyzeonly after a label is applied
(e.g.if: contains(github.event.pull_request.labels.*.name, 'review-ok')).
Cost notes
Wide diffs are expensive โ each file pays for an AI investigation. For
PRs against main, scope to the merge base (origin/main), not the
entire branch ancestry. If a touched file isn't worth investigating
(generated code, fixtures), add it to your existing ignore patterns or
drop it via a custom --files-from script:
git diff --name-only origin/main \
| grep -v '^generated/' \
| deepsec process --files-from -When NOT to use direct mode
- For the initial sweep of a large repo: full
scan+processorders
by noise tier, parallelizes better, and benefits from the
whole-repo signal in matcher gating. Direct mode is for incremental
review. - For revalidating existing findings: use
revalidatewith its own
filters.
File: docs/supported-tech.md
title: "Supported tech"
Canonical list of frameworks and ecosystems deepsec recognizes out of the box.
Each entry tells you three things:
- How deepsec detects it โ which sentinel files / lockfile shapes
trigger the tech tag. Seepackages/scanner/src/detect-tech.ts. - What it scans for โ the matcher slugs that activate when the tech
is detected. Matchers without a tech gate run on every repo. - What the prompt knows about โ the per-tech "threat highlights"
block that gets injected when the tech is detected. Seepackages/processor/src/prompt/highlights.ts.
Detection happens once per scan, with results persisted todata//tech.json. Matcher gates and prompt highlights share
that single signal.
Plugin authors: before adding a matcher for a framework already on
this list, check whether you can extend the existing matcher instead.
If your framework is missing, add a detector entry + matcher + prompt
highlight together in one PR.
TypeScript / JavaScript (Node, Bun, Deno, Workers)
Next.js (`nextjs`)
- Sentinel detection:
package.jsondepends onnext; ornext.config.{js,ts,mjs}is present. - Matchers:
all-route-handlers,all-server-actions,nextjs-middleware,nextjs-middleware-only-auth,framework-server-action,framework-untrusted-fetch,framework-internal-header,framework-image-optimizer,framework-edge-sandbox,page-data-fetch,page-without-auth-fetch,use-server-export,unsafe-json-in-html. - Prompt highlights: middleware.ts is not sufficient auth, Server
Actions are public POSTs, JSON-in-script XSS, search-param trust,
cache-tag cross-tenant leaks.
React (`react`)
- Sentinel detection:
reactorreact-dominpackage.json. - Matchers:
dangerous-html,xss,postmessage-origin. - Prompt highlights:
dangerouslySetInnerHTMLwith DB-stored HTML,
ref/effect-driven open redirects, JSON-in-script escapes.
Express (`express`)
- Sentinel detection:
expressinpackage.json. - Matchers:
js-express-route(gated), plus all generic JS matchers. - Prompt highlights: route-vs-middleware ordering,
req.*injection
surfaces,express.statictraversal, error-leak responses, CORS reflect.
Fastify (`fastify`)
- Sentinel detection:
fastifyinpackage.json. - Matchers:
js-fastify-route(gated). - Prompt highlights:
preHandler/onRequestauth, schema validation
as the FP mitigation, plugin scope inheritance.
NestJS (`nestjs`)
- Sentinel detection: any
@nestjs/*package inpackage.json. - Matchers:
js-nestjs-controller(gated). - Prompt highlights: missing
@UseGuards, untyped@Body(),@Public()opt-outs of global auth.
Hono (`hono`)
- Sentinel detection:
honoinpackage.json. - Matchers:
js-hono-route(gated). - Prompt highlights: middleware-before-routes ordering,
c.req.*
trust, edge-runtime trust boundary to backend.
Other JS detected (no dedicated matcher yet)
koa, hapi, remix, sveltekit, nuxt, astro, solidstart,trpc, mcp, connectrpc, graphql, socketio, bullmq, drizzle,prisma, bun, deno, workers. The generic JS/TS matchers
(all-route-handlers, cors-wildcard, secret-env-var, etc.) still run.
Python
Django / DRF (`django`, `djangorestframework`)
- Sentinel detection:
manage.py,Djangoinrequirements.txt/pyproject.toml/setup.py. - Matchers:
py-django-view(gated). - Prompt highlights:
@csrf_exempton writes, raw SQL via
f-strings,mark_safe,ModelFormmass assignment, DEBUG/ALLOWED_HOSTS
leaks, DRFpermission_classesgaps.
FastAPI (`fastapi`)
- Sentinel detection:
fastapiin deps. - Matchers:
py-fastapi-route(gated). - Prompt highlights: missing
Depends(...)auth,Optional[Any]
escape hatches, missingresponse_model,StaticFilestraversal.
Flask (`flask`)
- Sentinel detection:
flaskin deps. - Matchers:
py-flask-route(gated). - Prompt highlights:
@login_requireddecorator order, SSTI viarender_template_string, raw SQL viadb.engine.execute(f"..."),send_from_directorytraversal, hardcodedsecret_key.
Other Python detected
starlette, aiohttp, tornado, sanic, bottle, falcon,celery, airflow. Detection runs; dedicated matchers are roadmap.
PHP
Laravel (`laravel`)
- Sentinel detection:
composer.jsondepends onlaravel/*, orartisanscript present. - Matchers:
php-laravel-route(gated). - Prompt highlights: mass assignment via
$request->all(),DB::raw/whereRawSQL injection,VerifyCsrfToken::$exceptgaps,
Blade{!! !!}XSS, routes outside theauthmiddleware group.
Other PHP detected
symfony, slim, yii, cakephp, codeigniter, wordpress,drupal, magento. Roadmap.
Ruby
Rails (`rails`)
- Sentinel detection:
Gemfilementionsrails, orconfig/routes.rb/bin/railsexist. - Matchers:
rb-rails-controller(gated). - Prompt highlights:
skip_before_action :authenticate_user!,
strong-params bypasses,raw/html_safeXSS, raw SQL, open redirect.
Other Ruby detected
sinatra, grape, hanami, roda. Roadmap.
Go
Gin (`gin`)
- Sentinel detection:
go.modrequiresgithub.com/gin-gonic/gin. - Matchers:
go-gin-route(gated). - Prompt highlights: route-vs-middleware ordering,
c.Query/c.Param
trust, template auto-escaping vssafehtml.
Echo (`echo`)
- Sentinel detection:
go.modrequiresgithub.com/labstack/echo. - Matchers:
go-echo-route(gated). - Prompt highlights:
e.Useorder,c.Bindallowlists, group-level
middleware scope.
Fiber (`fiber`)
- Sentinel detection:
go.modrequiresgithub.com/gofiber/fiber. - Matchers:
go-fiber-route(gated). - Prompt highlights: middleware order, fasthttp body lifetime gotcha.
Chi (`chi`)
- Sentinel detection:
go.modrequiresgithub.com/go-chi/chi. - Matchers:
go-chi-route(gated). - Prompt highlights:
r.Mountmiddleware inheritance gotcha,chi.URLParamtrust, response-shape leakage.
Generic Go (`go`)
Always-on Go matchers regardless of framework: go-http-handler,go-ssrf, go-command-injection, go-embed-asset,connectrpc-handler-impl, proto-rpc-surface, unix-socket-listener.
Other Go detected
gorilla, buffalo, grpc, connectrpc, cobra. Roadmap for
dedicated matchers (gRPC service impl already partially covered).
Rust
Detection emits tags (actix, axum, rocket, warp, tide, poem,tonic, lambda-rs) but dedicated matchers are roadmap.
JVM (Java / Kotlin)
Detection emits jvm, plus spring, ktor, micronaut, jaxrs when
present. Matchers are roadmap.
.NET
Detection emits dotnet when a .csproj or global.json is present.
Matchers are roadmap.
Cross-cutting infra (always-on)
Tags docker, terraform, github-actions are emitted but don't gate
matchers โ the existing IaC and Dockerfile matchers (e.g.tf-iam-wildcard, dockerfile-from-mutable-tag,github-workflow-security) run unconditionally.
Adding a new ecosystem
Three pieces, in one PR:
- Detector โ add a branch in
packages/scanner/src/detect-tech.ts
that emits the tag from a sentinel file or dependency. - Matcher โ under
packages/scanner/src/matchers/<slug>.ts, withrequires: { tech: ["<tag>"] }so it only runs when detected. Register
it inpackages/scanner/src/matchers/index.ts. - Prompt highlight โ add an entry to
packages/processor/src/prompt/highlights.ts(3โ6 short bullet lines).
Don't write tutorials โ the model knows the framework.
Tests:
- A
detect-tech.test.tscase with a small fixture for the new tag. - A matcher unit test in
framework-matchers.test.ts(or sibling) with
a known-vulnerable input that produces matches and a known-safe input
that doesn't. - The existing
prompt-assemble.test.tsenforces a soft size cap on
highlights โ keep yours short.