deepsec Documentation

Deepsec is a security harness for finding vulnerabilities in your codebase powered by coding agents

5,159 stars TypeScript TypeScript 1 file ยท ~21,085 tokens
RAW Doc

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:

bash
npx deepsec init

The 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:

bash
npx deepsec init --max-cost-usd 100 --max-duration 2h

When the scan finishes, get a readable report:

bash
cd .deepsec
pnpm deepsec export --format md-dir --out ./findings

For later scans, work from inside .deepsec/:

bash
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 ./findings

The 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.

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:

bash
pnpm deepsec sandbox process --project-id my-app --sandboxes 10 --concurrency 4

Setup 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

text
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/report

deepsec 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

text
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.md plus
    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 to
    generated-matchers.ts for 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, write candidates to each FileRecord.
  • Cost: Free (no AI). ~15s for 2k files.
  • Inputs: Project root, matcher set (built-ins + plugin contributions).
  • Outputs: data//files/**/*.json with candidates populated and
    status: "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 into Findings, 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, or
    uncertain.
  • Cost: $$. Comparable to process. Worth running on HIGH+.
  • Inputs: Findings with no revalidation field, 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 in
packages/core/src/plugin.ts:

  • matchers โ€” additive
  • notifiers โ€” additive
  • agents โ€” additive
  • ownership โ€” single-slot (last plugin wins)
  • people โ€” single-slot
  • executor โ€” single-slot

A plugin registers via deepsec.config.ts:

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

  1. 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.

  2. Append-only analysis history. Re-running the processor doesn't
    overwrite past findings. It appends a new entry to analysisHistory
    and merges new findings (deduped by slug + title) into findings. You
    can re-run with a different agent, prompt, or model and get a strict
    improvement instead of a destructive replacement.

  3. 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.

ts
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.

ts
// 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 for
data//INFO.md and injects its contents into the prompt for
process, 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 from
generated-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

ts
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:

ts
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:

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:

ts
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.

text
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 reports

The scaffold keeps INFO.md and SETUP.md trackable, while ignoring
project.json, setup/, files/, runs/, and reports/. The generated
setup/ 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 reads
data/ directly. They live in
packages/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

text
pending     -- scan finished, awaits AI
   โ†“
processing  -- a run is currently investigating (lockedByRunId set)
   โ†“
analyzed    -- AnalysisEntry appended; findings updated

error is set if the agent crashed mid-investigation. Re-running
process 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:

bash
npx deepsec init

This 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/, run
pnpm deepsec init-project . Each project gets its own
data// 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, and
generated-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.

bash
MY_ANTHROPIC_KEY=... npx deepsec init \
  --agent claude --model-auth direct \
  --ai-provider anthropic --ai-api-key-env MY_ANTHROPIC_KEY

See 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:

  1. Revalidate HIGH+ before acting on findings. Worth the cost.
  2. Review setup's INFO.md and 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:

bash
# 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_PREFIX

The 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:

  • scan merges new candidates into existing FileRecords; doesn't
    re-investigate already-analyzed files.
  • process only touches files with status: "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).
  • revalidate only touches findings without a revalidation field
    unless --force is 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:

bash
npx deepsec init

The command asks you two things, then does the rest on its own:

  1. 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.
  2. 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:

bash
npx deepsec init

Deepsec 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:

bash
npx deepsec init --max-cost-usd 100 --max-duration 2h

When 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:

bash
cd .deepsec
pnpm deepsec export --format md-dir --out ./findings

That writes one markdown file per finding into ./findings. For a quick
overview instead:

bash
pnpm deepsec report

Everyday commands

After the first run, work from inside .deepsec/:

bash
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 ./findings

scan 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:

bash
MY_OPENAI_KEY=... npx deepsec init \
  --agent codex \
  --model-auth direct \
  --ai-provider openai \
  --ai-api-key-env MY_OPENAI_KEY

For 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:

bash
npx deepsec init --model-auth local

Deepsec 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:

bash
npx deepsec init --scaffold-only

This writes the .deepsec/ skeleton โ€” deepsec.config.ts,
package.json, README.md, AGENTS.md, .gitignore, and
data//{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, then
pnpm deepsec scan and pnpm deepsec process. You can also run
pnpm 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):

bash
pnpm deepsec sandbox process --project-id my-app --sandboxes 10 --concurrency 4

Sandboxes 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:

bash
# 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:

bash
VERCEL_TOKEN=... VERCEL_TEAM_ID=team_... VERCEL_PROJECT_ID=prj_... \
npx deepsec init --headless

Coding 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:

bash
pnpm deepsec init-project ../another-service --id another-service
pnpm deepsec setup --project-id another-service

When 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:

bash
npx deepsec init --agent codex --model gpt-5.5

For 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
bash
npx deepsec init --yes --model-profile value --output jsonl

Direct 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 or
codex 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

bash
# 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 and
revalidate. 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:

bash
pnpm deepsec process --project-id my-app --thinking-level high

Accepted 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:

bash
pnpm deepsec sandbox process --project-id my-app --sandboxes 30 --thinking-level high

Why 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:

bash
AI_GATEWAY_API_KEY=vck_...
pnpm deepsec process --project-id my-app --agent pi

Normal 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:

bash
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:bearer

Later 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 stay
pending (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 to
config.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:

bash
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-6

Two small integration points:

  1. 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.
  2. 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 to
    MODEL_PRICING_USD_PER_M_TOKENS in
    packages/processor/src/agents/codex-sdk.ts for 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 the
analysisHistory 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 in
packages/core/src/plugin.ts:

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:

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.

ts
// 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);
  },
};
ts
// 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:

ts
// 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 at
samples/webapp/deepsec.config.ts and
samples/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:

ts
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:

ts
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.

ts
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.

ts
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.

ts
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 in
packages/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:

ts
// 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.

bash
deepsec process --diff origin/main

How 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:

text
--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:

text
--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:

  1. Use --project-id if you pass one. If it's already declared in
    deepsec.config.ts, the declared root is used; otherwise --root
    (or the current working directory) is used.
  2. Otherwise, derive the id from the basename of the resolved root.
  3. Write data//project.json if it doesn't already exist.

Auto-creation is one-line and non-destructive โ€” it never modifies your
deepsec.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 on
if: hashFiles('comment.md') != ''.

This is the workflow we use to review our own PRs โ€” copy it as-is:

text
/* Detailed source-code truncated for AI context efficiency. */

How it works

  • Two-job 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: write but never runs any PR code โ€”
    it consumes only the sanitized comment.md artifact. 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
    under pull_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 so git 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 + setting
    CLAUDE_CODE_EXECUTABLE: claude skips the SDK's bundled-binary
    resolution, which can fail on Linux under some package managers.
  • pnpm deepsec โ€” swap for npx -y deepsec, npm exec deepsec,
    or yarn deepsec to match your package manager.
  • comment.md is uploaded only when findings exist โ€”
    --comment-out writes nothing on a green run, so the upload step's
    hashFiles check skips and the comment job 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: write to a job that runs PR code.
    The two-job pattern above keeps PR code in the no-write analyze
    job. If you're tempted to collapse them, remember that a PR can
    add arbitrary code to its own package.json postinstall 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, analyze has the secret in env while running
    PR-controlled pnpm install. The author_association gate is
    what prevents that from being a vulnerability. If you want
    defense-in-depth, run analyze only 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:

bash
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 + process orders
    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 revalidate with 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:

  1. How deepsec detects it โ€” which sentinel files / lockfile shapes
    trigger the tech tag. See packages/scanner/src/detect-tech.ts.
  2. What it scans for โ€” the matcher slugs that activate when the tech
    is detected. Matchers without a tech gate run on every repo.
  3. What the prompt knows about โ€” the per-tech "threat highlights"
    block that gets injected when the tech is detected. See
    packages/processor/src/prompt/highlights.ts.

Detection happens once per scan, with results persisted to
data//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.json depends on next; or
    next.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: react or react-dom in package.json.
  • Matchers: dangerous-html, xss, postmessage-origin.
  • Prompt highlights: dangerouslySetInnerHTML with DB-stored HTML,
    ref/effect-driven open redirects, JSON-in-script escapes.

Express (`express`)

  • Sentinel detection: express in package.json.
  • Matchers: js-express-route (gated), plus all generic JS matchers.
  • Prompt highlights: route-vs-middleware ordering, req.* injection
    surfaces, express.static traversal, error-leak responses, CORS reflect.

Fastify (`fastify`)

  • Sentinel detection: fastify in package.json.
  • Matchers: js-fastify-route (gated).
  • Prompt highlights: preHandler/onRequest auth, schema validation
    as the FP mitigation, plugin scope inheritance.

NestJS (`nestjs`)

  • Sentinel detection: any @nestjs/* package in package.json.
  • Matchers: js-nestjs-controller (gated).
  • Prompt highlights: missing @UseGuards, untyped @Body(),
    @Public() opt-outs of global auth.

Hono (`hono`)

  • Sentinel detection: hono in package.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, Django in
    requirements.txt/pyproject.toml/setup.py.
  • Matchers: py-django-view (gated).
  • Prompt highlights: @csrf_exempt on writes, raw SQL via
    f-strings, mark_safe, ModelForm mass assignment, DEBUG/ALLOWED_HOSTS
    leaks, DRF permission_classes gaps.

FastAPI (`fastapi`)

  • Sentinel detection: fastapi in deps.
  • Matchers: py-fastapi-route (gated).
  • Prompt highlights: missing Depends(...) auth, Optional[Any]
    escape hatches, missing response_model, StaticFiles traversal.

Flask (`flask`)

  • Sentinel detection: flask in deps.
  • Matchers: py-flask-route (gated).
  • Prompt highlights: @login_required decorator order, SSTI via
    render_template_string, raw SQL via db.engine.execute(f"..."),
    send_from_directory traversal, hardcoded secret_key.

Other Python detected

starlette, aiohttp, tornado, sanic, bottle, falcon,
celery, airflow. Detection runs; dedicated matchers are roadmap.

PHP

Laravel (`laravel`)

  • Sentinel detection: composer.json depends on laravel/*, or
    artisan script present.
  • Matchers: php-laravel-route (gated).
  • Prompt highlights: mass assignment via $request->all(),
    DB::raw/whereRaw SQL injection, VerifyCsrfToken::$except gaps,
    Blade {!! !!} XSS, routes outside the auth middleware group.

Other PHP detected

symfony, slim, yii, cakephp, codeigniter, wordpress,
drupal, magento. Roadmap.

Ruby

Rails (`rails`)

  • Sentinel detection: Gemfile mentions rails, or
    config/routes.rb / bin/rails exist.
  • Matchers: rb-rails-controller (gated).
  • Prompt highlights: skip_before_action :authenticate_user!,
    strong-params bypasses, raw/html_safe XSS, raw SQL, open redirect.

Other Ruby detected

sinatra, grape, hanami, roda. Roadmap.

Go

Gin (`gin`)

  • Sentinel detection: go.mod requires github.com/gin-gonic/gin.
  • Matchers: go-gin-route (gated).
  • Prompt highlights: route-vs-middleware ordering, c.Query/c.Param
    trust, template auto-escaping vs safehtml.

Echo (`echo`)

  • Sentinel detection: go.mod requires github.com/labstack/echo.
  • Matchers: go-echo-route (gated).
  • Prompt highlights: e.Use order, c.Bind allowlists, group-level
    middleware scope.

Fiber (`fiber`)

  • Sentinel detection: go.mod requires github.com/gofiber/fiber.
  • Matchers: go-fiber-route (gated).
  • Prompt highlights: middleware order, fasthttp body lifetime gotcha.

Chi (`chi`)

  • Sentinel detection: go.mod requires github.com/go-chi/chi.
  • Matchers: go-chi-route (gated).
  • Prompt highlights: r.Mount middleware inheritance gotcha,
    chi.URLParam trust, 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:

  1. Detector โ€” add a branch in packages/scanner/src/detect-tech.ts
    that emits the tag from a sentinel file or dependency.
  2. Matcher โ€” under packages/scanner/src/matchers/<slug>.ts, with
    requires: { tech: ["<tag>"] } so it only runs when detected. Register
    it in packages/scanner/src/matchers/index.ts.
  3. 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.ts case 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.ts enforces a soft size cap on
    highlights โ€” keep yours short.