← All posts
EXTENSION2026-10-2606 / 07

New scanners in. The gate, unchanged.

Every team has something the built-in scanners do not check: an in-house SAST tool, an SBOM policy, a coverage floor, a house rule about how migrations get written. Before now, that evidence had no way to reach the gate without re-implementing half the core. The new @veylqorix/plugin SDK exists to close exactly that gap, and no wider. You describe scanners and checks in a few lines, hand them to runPluginAudit, and their findings flow into the same runAudit, the same finding schema, and the same deterministic gate that the built-ins use.

a minimal plugin — finding ids may be omittedveylqorix
const myPlugin = definePlugin({
  id: 'acme/security',
  scanners: [{ id: 'acme-sast',
    run: async (ctx) => {
      const { code } = await ctx.exec('semgrep', ['run'], { cwd: ctx.root });
      return code === 0 ? [] : [{ scanner: 'acme-sast', ruleId: 'sast',
        title: 'semgrep found issues', severity: 'HIGH' }];
    },
  }],
});

You would hand that to runPluginAudit — the one-call path that wires the plugin’s evidence into runAudit and returns the gate result and exit code. Note the small cast the core plays on your findings: they are normalized by the core, not by you. Deterministic ids are assigned for anyone who omits them, severity and confidence are clamped into the schema, and malformed entries are dropped and counted. Report diffs stay stable, because the id algorithm is the one the built-in scanners use — a plugin cannot invent its own addressing scheme for findings.

Two invariants, in plain words

First: a broken plugin can never turn a bad scan into a clean pass. A plugin scanner or check that throws or times out is wrapped by the core and recorded as an ERROR result. Under the default policy the gate treats that error as blocking — the scan fails with exit 1 and the degraded coverage is recorded in the gate decision; where a degraded audit is the only issue, the verdict is INCOMPLETE, exit 2. Same shape as a broken built-in scanner: whichever exit code it lands on, it is not 0. The exit codes do not care who wrote the scanner.

Second: plugins add evidence only. There is no API surface for raising thresholds, suppressing findings, or overriding policy — that stays with the committed policy file and its capped, expirable exceptions. The gate remains single-sourced underneath all of it: the Rust engine when its binary is available, the byte-compatible TypeScript engine otherwise. And one job stays yours: reports trust that findings arrive already redacted. If your scanner sees secrets, mask the values before they leave it — redactSecret is exported from the core for exactly this — and mark the finding so the gate’s secret blocking applies.

  • Your scanners run in a context with exec — the core’s shell-free execFileAsync: no shell, a hard timeout, capped output. Plugin authors never receive a raw child_process handle.
  • Plugin scanners default to non-mandatory: a missing optional plugin tool is noted and degrades coverage, rather than creating the same completeness gap a missing built-in tool would.
  • skipQualityChecks skips plugin checks too — the “no check evidence reaches the gate” invariant holds across built-ins and plugins alike.

Honest status, the same as the CLI: @veylqorix/plugin (v0.1.0, MIT) lives in the monorepo at packages/plugin and is not published to npm yet — you consume it from source via workspace:*. It is covered by 20 SDK tests plus 7 core regression tests, which pin the two invariants above: a throwing plugin degrades coverage and never passes, and HIGH findings block identically on either gate authority. docs/plugin.md has the full authoring guide and API reference.