## File: README.md

# deepsec

[`deepsec`](https://deepsec.sh) 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](https://github.com/vercel-labs/deepsec/blob/main/docs/models.md)), 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](https://github.com/vercel-labs/deepsec/blob/main/docs/getting-started.md)
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](https://github.com/vercel-labs/deepsec/blob/main/docs/getting-started.md) — set up and run your first scan
- [Reviewing changes](https://github.com/vercel-labs/deepsec/blob/main/docs/reviewing-changes.md) — `process --diff` and CI gating
- [Supported technology](https://github.com/vercel-labs/deepsec/blob/main/docs/supported-tech.md) — built-in coverage
- [Generated and hand-authored matchers](https://github.com/vercel-labs/deepsec/blob/main/docs/writing-matchers.md)
- [Configuration](https://github.com/vercel-labs/deepsec/blob/main/docs/configuration.md)
- [Plugins](https://github.com/vercel-labs/deepsec/blob/main/docs/plugins.md)
- [Models](https://github.com/vercel-labs/deepsec/blob/main/docs/models.md)
- [Project link and credentials](https://github.com/vercel-labs/deepsec/blob/main/docs/vercel-setup.md)
- [Architecture](https://github.com/vercel-labs/deepsec/blob/main/docs/architecture.md)
- [Data layout](https://github.com/vercel-labs/deepsec/blob/main/docs/data-layout.md)
- [FAQ](https://github.com/vercel-labs/deepsec/blob/main/docs/faq.md)
- [Samples](https://github.com/vercel-labs/deepsec/tree/main/samples)
- [Contributing](https://github.com/vercel-labs/deepsec/blob/main/CONTRIBUTING.md)

## 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](https://github.com/vercel-labs/deepsec/blob/main/docs/vercel-setup.md)
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](https://vercel.com/docs/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](LICENSE) and [NOTICE](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/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

```
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](data-layout.md).

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 `Finding`s, 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`](https://github.com/vercel-labs/deepsec/blob/main/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](plugins.md) 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`](https://github.com/vercel-labs/deepsec/blob/main/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](models.md). |
| `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](getting-started.md) 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](writing-matchers.md).

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

```
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`](https://github.com/vercel-labs/deepsec/blob/main/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](getting-started.md) 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](models.md#refusals-are-not-a-large-problem). |
| `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 updated
```

`error` is set if the agent crashed mid-investigation. Re-running
`process` will retry `error` and `pending` files.

## `runs/<runId>.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](getting-started.md) 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](vercel-setup.md) 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][sb] 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.

[sb]: https://vercel.com/docs/sandbox

## 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](writing-matchers.md). 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](https://github.com/vercel-labs/deepsec/blob/main/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](#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](vercel-setup.md) 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](writing-matchers.md).

## 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](reviewing-changes.md),
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](configuration.md) — project, model route, and
  environment configuration.
- [writing-matchers](writing-matchers.md) — improving scan coverage
  with your own matchers.
- [vercel-setup](vercel-setup.md) — credentials, CI setups, and
  troubleshooting the Vercel connection.
- [architecture](architecture.md) — 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](https://vercel.com/ai-gateway/leaderboards/deepsecbench/results.json),
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](vercel-setup.md).

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`](https://github.com/vercel-labs/deepsec/blob/main/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](writing-matchers.md) 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`](https://github.com/vercel-labs/deepsec/blob/main/samples/webapp/deepsec.config.ts) and
[`samples/webapp/matchers/`](https://github.com/vercel-labs/deepsec/tree/main/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/`](https://github.com/vercel-labs/deepsec/tree/main/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:

```
/* 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](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions).
- **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.