> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fallow.tools/llms.txt
> Use this file to discover all available pages before exploring further.

# fallow security

> CLI reference for fallow security. Surface opt-in local security candidates for agent or human verification.

Surface local security candidates for verification. Two rule families ship: the graph-structural `client-server-leak` (a `"use client"` file that directly reads, or transitively imports a module that reads, a non-public `process.env` secret), and the data-driven `tainted-sink` catalogue (syntactic dangerous-sink candidates across a catalogue of CWE categories). Both default to `off` and run only under `fallow security`.

<Note>
  Findings are candidates, not confirmed vulnerabilities. Fallow reports a structural trace so an agent or human can verify whether the value can actually reach client-bundled code.
</Note>

```bash theme={null}
fallow security
```

## Options

### Output

| Flag                    | Description                                                                  |
| :---------------------- | :--------------------------------------------------------------------------- |
| `-f, --format <FORMAT>` | Output format: `human` (default), `json`, or `sarif`                         |
| `-q, --quiet`           | Suppress progress output                                                     |
| `--summary`             | Show a compact human summary instead of per-finding detail                   |
| `--ci`                  | CI mode: equivalent to `--format sarif --fail-on-issues --quiet`             |
| `--fail-on-issues`      | Exit with code 1 if security candidates are found                            |
| `--sarif-file <PATH>`   | Write SARIF output to a file in addition to the primary output               |
| `--legacy-envelope`     | Emit JSON without the top-level `kind` discriminator for one migration cycle |

### Scoping

| Flag                                      | Description                                                                                                                                                                                    |
| :---------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-r, --root <PATH>`                       | Project root directory (default: current working directory)                                                                                                                                    |
| `-c, --config <PATH>`                     | Path to config file (default: auto-detected)                                                                                                                                                   |
| `--changed-since <REF>` (alias: `--base`) | Only report candidates whose client anchor or trace hops touch files changed since a git ref                                                                                                   |
| `--file <PATH>`                           | Only report candidates whose finding anchor or trace hop matches the selected file. Repeat to select multiple files. The full graph is still analyzed                                          |
| `--diff-file <PATH>`                      | Narrow candidates to added hunks on the client anchor or import trace. Secret-source hops use file-level retention because member-access spans are not yet stored. Use `-` to read from stdin. |
| `--diff-stdin`                            | Read the unified diff from stdin                                                                                                                                                               |
| `-w, --workspace <PATTERNS>`              | Scope output to selected workspace packages                                                                                                                                                    |
| `--changed-workspaces <REF>`              | Scope output to workspace packages touched since the given git ref                                                                                                                             |

### Performance

| Flag            | Description                 |
| :-------------- | :-------------------------- |
| `--no-cache`    | Disable incremental caching |
| `--threads <N>` | Number of parser threads    |

### Regression gate

| Flag         | Description                                                                                                                                                                                                                  |
| :----------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--gate new` | Fail (exit code **8**) only when the change introduces a NEW security-sink candidate in the changed lines, not on the whole candidate backlog. Requires a diff source (`--changed-since`, `--diff-file`, or `--diff-stdin`). |

The gate is the first-line-of-defence form of `fallow security`: a refactor that merely
touches a file already containing a sink passes, while a change that adds a new sink (or
wires a new untrusted source into an existing sink) on a changed line fails. Findings stay
**unverified candidates**: the human output says `REVIEW REQUIRED` (not `FAIL`), SARIF keeps
every result at `level: note` with the verdict in `run.properties.fallowGate`, and
`--format json` carries an additive `gate` block (`mode` / `verdict` / `new_count`).

Exit codes: **8** = a new candidate was introduced in the changed lines; **0** = clean (or a
docs-only / empty diff); **2** = the gate could not compute the diff (an unfetched ref on a
shallow clone, a bad ref, not a git repo). Exit 8 is dedicated and stable, so a pipeline can
soft-gate it without allow-listing real errors (GitLab `allow_failure: exit_codes: [8]`).

<Note>
  On a shallow clone the merge-base may not be fetched. In GitHub Actions set
  `fetch-depth: 0` on `actions/checkout`; in GitLab CI set `GIT_DEPTH: 0`. The gate exits 2
  (loud) rather than passing silently when it cannot compute the diff.
</Note>

```bash CI gate on changed lines theme={null}
# GitHub Action / generic CI: gate the PR's committed range
fallow security --gate new --changed-since "$BASE_SHA"

# Pre-commit hook: gate the STAGED content (not committed HEAD)
git diff --cached --unified=0 | fallow security --gate new --diff-stdin
```

## Rule: client-server-leak

The detector starts at files with a top-level `"use client"` directive and walks static imports. It reports a candidate when the client boundary can reach a module that reads a non-public `process.env` value.

Public-by-convention env values are excluded:

| Public prefix   | Example                            |
| :-------------- | :--------------------------------- |
| `NODE_ENV`      | `process.env.NODE_ENV`             |
| `NEXT_PUBLIC_*` | `process.env.NEXT_PUBLIC_API_URL`  |
| `VITE_*`        | `process.env.VITE_API_URL`         |
| `NUXT_PUBLIC_*` | `process.env.NUXT_PUBLIC_SITE_URL` |
| `REACT_APP_*`   | `process.env.REACT_APP_API_URL`    |
| `PUBLIC_*`      | `process.env.PUBLIC_SITE_URL`      |
| `GATSBY_*`      | `process.env.GATSBY_SITE_URL`      |
| `EXPO_PUBLIC_*` | `process.env.EXPO_PUBLIC_API_URL`  |
| `STORYBOOK_*`   | `process.env.STORYBOOK_THEME`      |

Dynamic `import()` edges that the graph cannot follow are counted in the output as unresolved edge files. A clean finding list with a non-zero unresolved count is not a clean bill.

## Rule: tainted-sink (catalogue)

A data-driven catalogue of syntactic sink candidates. Where `client-server-leak` is a graph-reachability rule, `tainted-sink` flags a call, member assignment, or tagged template that reaches a known dangerous sink. Most rows require a **non-literal** argument; narrowly literal-aware rows flag deterministic unsafe literals such as wildcard `postMessage` origins, weak crypto algorithms, disabled TLS validation, and JWT algorithm issues.

All catalogue findings carry `kind: "tainted-sink"` plus a `category` (the catalogue id) and a `cwe` number. The catalogue ships these categories:

| Category                         | CWE  | Sink shape                                                                                                                                                                   |
| :------------------------------- | :--- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dangerous-html`                 | 79   | `innerHTML` / `outerHTML` / `insertAdjacentHTML` / `dangerouslySetInnerHTML`                                                                                                 |
| `template-escape-bypass`         | 79   | template-engine `SafeString(...)` wrapping a non-literal value                                                                                                               |
| `command-injection`              | 78   | `child_process` `exec` / `execSync` / `spawn` / `spawnSync` (import-provenance gated)                                                                                        |
| `code-injection`                 | 94   | `eval` / `vm.runInNewContext`                                                                                                                                                |
| `dynamic-regex`                  | 1333 | `RegExp(...)` / `new RegExp(...)` with a non-literal pattern                                                                                                                 |
| `redos-regex`                    | 1333 | vulnerable regex literals tested with source-backed input                                                                                                                    |
| `resource-amplification`         | 400  | source-backed size into `Array(...)` / `new Array(...)` / `Buffer.alloc*` / `String.prototype.repeat` / `padStart` / `padEnd` (directly `Math.min`-clamped sizes stay quiet) |
| `dynamic-module-load`            | 95   | dynamic `require(...)`                                                                                                                                                       |
| `sql-injection`                  | 89   | `query` / `execute` with concatenation or interpolation, raw escape hatches (`sql.raw`, Prisma unsafe raw, Knex raw, `sequelize.literal`)                                    |
| `ssrf`                           | 918  | `fetch` / `got` / `ky` / `needle` / `request` / `axios` / `superagent` / `undici` / `http(s).request`                                                                        |
| `path-traversal`                 | 22   | `path.join` / `path.resolve` / `node:fs` path methods / route `sendFile`                                                                                                     |
| `header-injection`               | 113  | response `setHeader` / `writeHead`                                                                                                                                           |
| `open-redirect`                  | 601  | `res.redirect` / `location.href` / `location.assign` / `window.open`                                                                                                         |
| `postmessage-wildcard-origin`    | 346  | `postMessage(..., "*")`                                                                                                                                                      |
| `tls-validation-disabled`        | 295  | HTTPS/TLS options with `rejectUnauthorized: false`, plus `NODE_TLS_REJECT_UNAUTHORIZED = "0"`                                                                                |
| `cleartext-transport`            | 319  | cleartext `http://` URLs in fetch-like calls and WebSocket constructors                                                                                                      |
| `electron-unsafe-webpreferences` | 1188 | Electron `webPreferences` with unsafe literal options                                                                                                                        |
| `world-writable-permission`      | 732  | `chmod` / `chmodSync` with world-writable modes                                                                                                                              |
| `insecure-temp-file`             | 377  | predictable temporary file paths in `fs` writes                                                                                                                              |
| `mysql-multiple-statements`      | 89   | MySQL connection options with `multipleStatements: true`                                                                                                                     |
| `permissive-cors`                | 942  | CORS wildcard origin with credentials                                                                                                                                        |
| `insecure-cookie`                | 614  | cookie options missing or disabling `httpOnly` / `secure`                                                                                                                    |
| `mass-assignment`                | 915  | source-backed `Object.assign(target, source)`                                                                                                                                |
| `weak-crypto`                    | 327  | runtime-selectable hash or cipher algorithm                                                                                                                                  |
| `deprecated-cipher`              | 327  | `crypto.createCipher` / `createDecipher` (no IV, MD5-based KDF)                                                                                                              |
| `insecure-randomness`            | 338  | `crypto.pseudoRandomBytes(...)`                                                                                                                                              |
| `unsafe-buffer-alloc`            | 1188 | `Buffer.allocUnsafe` / `allocUnsafeSlow` (uninitialized memory)                                                                                                              |
| `unsafe-deserialization`         | 502  | `js-yaml` `load` / `node-serialize`                                                                                                                                          |
| `prototype-pollution`            | 1321 | `__proto__` writes and recursive merge sources                                                                                                                               |
| `zip-slip`                       | 22   | archive extraction destination paths                                                                                                                                         |
| `nosql-injection`                | 943  | Mongo / Mongoose query object passthrough                                                                                                                                    |
| `ssti`                           | 1336 | template engine compile / render calls                                                                                                                                       |
| `xxe`                            | 611  | XML parse calls                                                                                                                                                              |
| `secret-pii-log`                 | 532  | source-backed secrets or request PII reaching logs                                                                                                                           |
| `hardcoded-secret`               | 798  | provider-prefix credentials and high-entropy literals assigned to secret-shaped identifiers (include-required)                                                               |
| `xpath-injection`                | 643  | `xpath.select` / `select1` with a non-literal expression                                                                                                                     |
| `jwt-alg-none`                   | 347  | JWT signing with algorithm `none`                                                                                                                                            |
| `jwt-verify-missing-algorithms`  | 347  | `jsonwebtoken` verify calls missing an `algorithms` allowlist                                                                                                                |
| `webview-injection`              | 94   | react-native-webview `injectJavaScript(...)` / `injectedJavaScript=` (enabler-gated)                                                                                         |
| `angular-trusted-html`           | 79   | Angular `bypassSecurityTrust*` (enabler-gated)                                                                                                                               |
| `nextjs-open-redirect`           | 601  | Next.js `redirect` / `permanentRedirect` (enabler-gated)                                                                                                                     |
| `dom-document-write`             | 79   | `document.write` / `document.writeln`                                                                                                                                        |
| `jquery-html`                    | 79   | jQuery `.html(value)` (enabler-gated)                                                                                                                                        |
| `route-send-file`                | 22   | Express / Fastify / Hono route `sendFile` (enabler-gated)                                                                                                                    |

<Note>
  These are deliberately conservative candidates: a non-literal argument is a signal to verify, not proof of a vulnerability. Fallow does not prove the value is attacker-controlled or reaches the sink unsanitized. Verification is the agent's job.
</Note>

Sink-shaped nodes whose callee cannot be resolved to a static path (dynamic dispatch, computed members, aliased bindings) are counted in the output as `unresolved_callee_sites`. As with `client-server-leak`, a clean finding list with a non-zero count is not a clean bill.

### Enabling categories

`tainted-sink`, `hardcoded-secret`, and `client-server-leak` default to `off` and are surfaced only by `fallow security` (never under bare `fallow` or the `audit` gate). Scope which catalogue categories run with `security.categories` in config:

```json theme={null}
{
  "security": {
    "categories": {
      "include": ["dangerous-html", "command-injection", "hardcoded-secret"],
      "exclude": []
    }
  }
}
```

With both lists empty, ordinary catalogue categories are active. `hardcoded-secret` is intentionally include-required and only runs when listed in `security.categories.include`.

## Suppression

Suppress a known false positive at file level. Each rule has its own token:

```ts theme={null}
// fallow-ignore-file security-client-server-leak
"use client";
```

```ts theme={null}
// fallow-ignore-file security-sink
const el = document.querySelector(".out");
el.innerHTML = render(userInput);
```

One `security-sink` token covers every catalogue category. Use suppression only after verifying that the value cannot reach the sink unsanitized, for example because the input is a trusted constant, server-only, or sanitized upstream.

## JSON output

`--format json` emits a typed root envelope with `kind: "security"` unless `--legacy-envelope` is set.

```json theme={null}
{
  "kind": "security",
  "schema_version": "1",
  "security_findings": [],
  "unresolved_edge_files": 0,
  "unresolved_callee_sites": 0
}
```

Each finding includes `kind`, `path`, `line`, `col`, `evidence`, `trace`, `actions`, and optional `reachability`. `tainted-sink` findings additionally carry `category` (the catalogue id, for example `"dangerous-html"`) and `cwe` (the category's CWE number); `client-server-leak` findings omit both. `tainted-sink` findings can also include `reachability.untrusted_source_trace` when a module with a known untrusted source imports the sink module. It is ranking and triage context only, not proof that a specific value reaches the sink.

### Agent-actionable candidate record

Every finding also carries a `candidate` record, an optional `taint_flow` triple, and a stable `finding_id`, designed for an agent to act on without re-deriving anything from the evidence string.

```json theme={null}
{
  "finding_id": "9a705395d3b50465",
  "kind": "tainted-sink",
  "category": "command-injection",
  "cwe": 78,
  "path": "src/runner.ts",
  "line": 4,
  "col": 2,
  "candidate": {
    "source_kind": null,
    "sink": {
      "path": "src/runner.ts",
      "line": 4,
      "col": 2,
      "category": "command-injection",
      "cwe": 78,
      "callee": "child_process.exec"
    },
    "boundary": { "client_server": false, "cross_module": true }
  },
  "taint_flow": {
    "source": { "path": "src/route.ts", "line": 1, "col": 9 },
    "sink": { "path": "src/runner.ts", "line": 4, "col": 2 },
    "path": { "intra_module": false, "cross_module_hops": 1 }
  }
}
```

The `candidate` record has three slots:

* `source_kind`: the kind of untrusted input that reaches the sink, as a stable catalogue source id such as `"http-request-input"`, `"process-env"`, `"process-argv"`, `"message-event-data"`, or `"location-input"`. Absent when no untrusted source matched (always absent for `client-server-leak`). Treat an unknown id as an untrusted source of unknown kind; never drop a candidate on that basis.
* `sink`: a self-contained description of the sink site (`path`, `line`, `col`, `category`, `cwe`, and the captured `callee`), so you can act on `candidate.sink` without reading the rest of the finding.
* `boundary`: whether the flow crosses a `client_server` boundary (a `"use client"` file in the trace) or a `cross_module` boundary (the source reaches the sink across one or more import hops), plus an `architecture_zone` (`from`/`to`) when the anchor also participates in a declared architecture-boundary violation.

There is no `impact` field. Deciding exploitability and severity is the verifying agent's job, not fallow's; fallow flags candidates with structural evidence.

`taint_flow` is present only when an untrusted source is import-reachable to the sink. It is the `{ source, sink, path }` shape agent SAST tooling expects. `path` is a compact summary (`intra_module` and `cross_module_hops`); the full ordered hop list lives in `reachability.untrusted_source_trace` and is not duplicated.

`finding_id` is a stable correlation id, identical across runs for the same rule, path, and line, and identical to the finding's SARIF `partialFingerprints` value. Use it to track a candidate across runs (for example after a rebase) and to join JSON and SARIF output.

## Examples

<CodeGroup>
  ```bash Basic scan theme={null}
  fallow security
  ```

  ```bash JSON for agents theme={null}
  fallow security --format json --quiet
  ```

  ```bash GitHub Code Scanning theme={null}
  fallow security --ci --sarif-file fallow-security.sarif
  ```

  ```bash PR-scoped scan theme={null}
  git diff --unified=0 origin/main...HEAD | fallow security --diff-file -
  ```
</CodeGroup>
