Antivirus for training data

A local scanner flags poison. Anyone can confirm a flag. Confirmed poison becomes a public signature the next scan gets for free. The repo is the product: no login, no hosted service.

antiserum scan ./data
make reproduce

make reproduce scans the week 11–12 reference mix (corpus/reference/) and exits nonzero if a planted row is missed. corpus/toy/ is the two-minute demo.

This site is public documentation. It does not scan, accept corpora, run a judge, or take accounts.

Local-first (hard)

Install

Python 3.10+. From a checkout:

python3 -m pip install -e ".[dev]"

That exposes the antiserum command (python3 -m antiserum also works). Arrow / Parquet shards in a local Hugging Face cache need the optional extra (pip install -e ".[hf]"), unused unless those files are on the path you pass.

Without a checkout:

pip install "antiserum @ git+https://github.com/antiserum-ai/antiserum.git"

Install from this repo or that git URL. This page does not document a PyPI package.

Scan

JSONL: one object per line. Optional id and label. Checks run on the concatenated text (text; Alpaca instruction / input / output; ShareGPT messages / conversations; Hugging Face prompt + completion). .csv and JSON-array .json with those headers ingest the same way, including .gz via stdlib gzip. Plain .txt: one file, one record. Unknown shapes fail with a one-line fix: add a string text field.

v0 loads the mix in process. Default ceiling: 25,000 rows or 128 MiB. A stop before the path is exhausted is truncated: the receipt records which ceiling, records seen, and bytes seen, and scan exits 3. --allow-truncated keeps exit 0 for a deliberate sample. Raise --max-records / --max-bytes if this machine can hold the mix. --progress writes a one-line ingest counter to stderr (auto on a TTY). Progress never goes on stdout and does not change the receipt or exit codes.

antiserum scan ./data
antiserum scan ./data --out receipt.json
antiserum scan ./data --json
antiserum scan ./data --sarif antiserum.sarif
antiserum scan ./data --html report.html
antiserum scan ./data --csv findings.csv
antiserum scan ./data --fail-on any
antiserum scan ./data --allowlist allowlist.jsonl
antiserum scan ./data --only-checks signature_hit,hidden_unicode
antiserum scan ./data --skip-checks stat_outliers
antiserum checks
antiserum checks --json
antiserum scan ./data --max-records 50000
antiserum scan ./data --max-records 100 --allow-truncated
antiserum scan ./data --progress
antiserum scan --help
antiserum init
antiserum init ./data
antiserum init --force

--only-checks and --skip-checks cannot be combined. Unknown names exit 2 and list the known checks. antiserum checks prints the built-in names (one per line, default_checks() order; --json writes {"checks":[...]}). In-process catalog only. The receipt records which checks ran.

Optional local antiserum.toml next to the scan path or in the current working directory sets fail_on, only_checks / skip_checks, max_records / max_bytes, allowlist, and allow_truncated. antiserum init writes a starter file with those keys as commented defaults; --force overwrites. First file found wins (scan path, then cwd). --config PATH uses that local file only and skips the search. PATH must be a readable file (exit 2 if missing or unreadable). CLI flags override the file. Unknown keys exit 2. Missing file is fine when --config is omitted. The receipt records path + hash. Local file only; never fetched.

Live antiserum scan corpus/toy on the planted toy mix (45 records — not a production corpus):

Output of antiserum scan corpus/toy on the planted toy mix

Exit codes

CodeMeaning
0Ran. No flags at or above the --fail-on threshold.
1One or more flags at or above the --fail-on threshold.
2Usage or I/O error.
3Scan stopped at --max-records / --max-bytes before the path was exhausted. Distinct from --fail-on. --allow-truncated keeps exit 0.

--fail-on {any,high,never} is the severity gate (default: never).

Outputs

ArtifactHowWhat it is
Text receiptstdout (default)Deterministic summary: dataset hash, scanner version, pack identity, flags. Meant to paste into a model card.
JSON receipt--out receipt.json or --jsonSame facts as the text receipt. flags[].severity is enough to fail a job without scraping text. Shape: receipt.schema.json (local file; no hosted registry).
SARIF 2.1.0--sarif antiserum.sarifEach flag is a result. Upload from your runner with github/codeql-action/upload-sarif if you want GitHub code scanning. The upload goes to GitHub on that runner, not to us.
HTML report--html report.htmlSelf-contained findings report: counts by check and severity, each flag, pack/receipt identity. Inline CSS; no CDN. Local file only.
CSV table--csv findings.csvOne row per flag: record_id, check, severity, reason, source, line. Empty scan writes the header only.
JUnit XMLantiserum eval --junit junit.xmlPer-check plant recall / clean FP vs corpus/reference/thresholds.json. A floor or ceiling miss is a failure. Not a scan export.

Receipt JSON/text is unchanged by --sarif, --html, and --csv. All of these are local files. There is no hosted report store.

Reusable Action (CLI on the caller runner; no API key):

jobs:
  scan:
    uses: antiserum-ai/antiserum/.github/workflows/scan.yml@main
    with:
      path: ./data
      fail-on: any

Confirm

A stranger should be able to do this without asking us. No form, no login.

antiserum scan corpus/toy --out receipt.json
antiserum judge corpus/toy --receipt receipt.json --out judgments.json
antiserum confirm --judgments judgments.json
antiserum allowlist add --judgments judgments.json --path corpus/toy
antiserum propose --judgments judgments.json
antiserum export-leftovers judgments.json --out packet.json
antiserum import-decisions decisions.json --into judgments.json

judge is an offline first-pass. No API key. allowlist add appends settled false_alarm rows to a local allowlist.jsonl (idempotent; no cloud list). propose prints the next AS-YYYY-NNNN line and a PR body. export-leftovers / import-decisions move leftover packets as local files. Rubric: confirm.md. Optional leftover packet (local CLI; no live PoQ wire): leftover-packet.schema.json, poq-leftover-review.md.

What it flags

CheckWhat it catches
trigger_ngramsRare token sequences, including pipe-wrapped canaries like |prod|
label_flipsCoordinated rows that invert a label in a tight cluster
duplicate_injectNear-copy dumps used to overweight a planted example
paraphrase_overweightShared-phrase families Jaccard does not already cluster
stat_outliersLength, entropy, or alphabet spikes vs the mix
signature_hitMatch against feed/signatures.jsonl (NFKC text)
instruction_overrideA single SFT / chat row that teaches "ignore previous instructions"
hidden_unicodeUnicode Tags, bidi overrides, or zero-width payload separators
mixed_scriptA word token that mixes Latin with Cyrillic, Greek, or other lookalike scripts
pair_triggerTwo rare distinctive phrases that co-occur in the same row

How to implement another check: checks.md. Honest coverage: threat-model.md.

Deep docs

DocWhat it is
threat-model.mdAttacker, asset, non-claims, field-hunt notes
checks.mdCheck contract and how to add one
field-hunt.mdStanding hunt: local fetch, catch/miss, next mixes
confirm.mdRubric, first-pass rules, leftover loop
leftover-packet.schema.jsonOptional leftover-review packet (antiserum.leftover_packet.v1). Local CLI; no live PoQ wire.
poq-leftover-review.mdLeftover packet + Sapien locks (queue, 30-day TTL, judgment-store export)
signatures.mdFeed line format and review bar
positioning.mdCategory neighbors and non-claims
receipt.schema.jsonPublished shape of antiserum scan --out. Local file; no hosted registry.
UpdatesDated capability notes (Markdown in docs/blog/)
READMEFull CLI contract on main

What this is not

The local scan of the text mix you are about to train on. Not a runtime prompt firewall, not a pickle / weight malware scanner, not a data-quality suite, not a weight-level backdoor inverter. Text only. A clean receipt is not a proof the mix is safe and does not prove a downloaded base model is clean.

Source: github.com/antiserum-ai/antiserum. MIT.