AdapterSentry
AdapterSentry is a static security scanner for LoRA adapters distributed as .safetensors
files. A LoRA adapter is a small file that changes how a large language model behaves; anyone
can publish one, and a poisoned adapter can carry a backdoor that stays silent until a trigger
appears in the input. AdapterSentry inspects the adapter's weights before the adapter is
loaded, without running the model and without knowing the trigger.
v2.0.0 replaces the analysis engine: exact statistics of the effective update
ΔW = s·B·A (no sampling, no proxies), streaming analysis that scales to 70B–405B and MoE
models in tens of megabytes of memory, a comparison of every module with its own family inside
the adapter, structural findings, and a new result contract (ScanResult 2.x).
v2.0.1 recalibrates the intra-adapter verdict rule on 393 real adapters from the Hugging
Face Hub (see Limits).
See CHANGELOG.md and Migrating from 1.x.
What it tells you
$ adaptersentry scan adapter_model.safetensors
AdapterSentry 2.0.1 · adapter_model.safetensors
================================================================
Verdict: REVIEW (MEDIUM) · confidence medium · behavioural check recommended
Status: ok · policy default · mode full
Why:
- [LOW] NO_REFERENCE_PROFILE: No calibrated reference for this model family; verdict relies on intra-adapter comparison
- [MEDIUM] INTRA_OUTLIER: Update concentrated on a few outputs, unlike the rest of its family (down_proj layer 47): concentration z = 21.3 ≥ 14.0 (threshold set at 5 % alerts on popular Hub adapters)
Adapter: peft_lora, family llama (config), 80 layers, r=64 (declared 64), α=128, scaling alpha_over_r, training trained
Coverage: 560/560 modules analysed; 0 tensor(s) not analysed
Intra: concentration z 21.3 · max single-feature z 64.0 · 9 outlier module(s) · energy Gini 0.41
Findings (1):
[MEDIUM] INTRA_CONCENTRATION_SPIKE — Update concentrated on a few outputs, unlike its family (layer 47, down_proj)
(Illustrative output.)
The verdict is one of allow, review or block, with the reasons, a confidence
level and what limits it. CI gates read verdict.action.
Quick start
pip install adaptersentry
adaptersentry scan adapter_model.safetensors # text summary
adaptersentry scan adapter_model.safetensors --format json # ScanResult 2.1.0
adaptersentry scan adapter_model.safetensors --format sarif \
--output adaptersentry.sarif --fail-on review # GitHub code scanning + CI gate
adaptersentry batch --input-dir adapters/ --workers 8 --fail-on review
from pathlib import Path
from adaptersentry import scan
result = scan(Path("adapter_model.safetensors"))
print(result.verdict.action, result.verdict.level)
for reason in result.verdict.reasons:
print(reason.code, reason.message)
Put adapter_config.json next to the .safetensors file (as PEFT saves it): the scan needs
lora_alpha to know the real size of the update.
| Option | Meaning |
|---|---|
--format text|json|full-json|sarif |
output; full-json adds per-module features |
--policy default|strict |
strict blocks on structural red flags (see below) |
--fail-on review|block |
exit code 2 if the verdict reaches this action |
--mode full|fast |
fast uses fewer samples for the kurtosis estimate |
--include-modules, --include-heads |
per-module / per-attention-head detail in JSON |
--full-paths |
report the absolute path instead of the file name |
Exit codes: 0 completed, 1 the adapter could not be analysed (the verdict is still
review), 2 the --fail-on threshold was reached.
How it works
file ─► validate ─► inventory ─► per module (streamed) ─► compare within ─► structural ─► verdict
header every exact ΔW features each module findings
& limits tensor via r×r cores family
- Validate before reading. The file must be a regular
.safetensorsfile; the header length, dtype, shape, byte ranges, element counts and LoRA rank are checked from the header before any tensor is allocated. - Inventory. Every tensor is classified: LoRA A/B pairs (with layer, expert and module),
full weights shipped via
modules_to_save, DoRA vectors, trainable-token deltas, and anything that cannot be analysed — nothing is skipped silently. - Exact features, one module at a time. For ΔW = s·B·A, where s = α/r (or α/√r with
rsLoRA) comes from
adapter_config.json, the singular values, norms and row/column norms are computed exactly through the r×r matrices AAᵀ and BᵀB — ΔW itself (235 M values for a 70B MLP layer) is never built. Features describe the shape of the update, which the literature finds informative, rather than its size: stable rank, participation ratio, spectral entropy, concentration of the leading direction versus a random baseline, entry kurtosis (estimated by importance sampling of rows so rare spiky rows are not missed), and how concentrated the update is on a few output rows or input columns — forlm_headand embeddings, on a few tokens. - Compare within the adapter. Each module is compared with its family (all
q_proj, alldown_proj, …): the trend with depth is removed with a Theil–Sen line, then robust z-scores (median/MAD) are computed for every feature. Real adapters differ a lot between modules of one family, so a single unusual feature is reported but does not change the verdict. The verdict looks for one pattern: a module whose update is concentrated on a few outputs — entry kurtosis, top-8 row share and row Hoyer all rise against the family (concentration z). This needs no reference data and is insensitive to the adapter's overall strength. It runs for families of at least 16 modules. - Structural findings that numbers cannot show: complete
lm_head/ embedding / router matrices shipped in the adapter, LoRA on a mixture-of-experts router, ranks that differ from the declaredr(typical of merged adapters), code/pickle/archive files next to the adapter. - Verdict — one rule set, fail-closed:
| Situation | Action |
|---|---|
| File could not be parsed, or part of it was not analysed | at least review |
| A module's update is concentrated on a few outputs, unlike its family (concentration z ≥ 14) | review |
| Structural red flag | review; block with --policy strict |
| Reference profile for the model family says p ≤ α | review / block (reference profiles: next release) |
| Otherwise | allow |
Without a calibrated reference profile the default policy never blocks: the false-positive rate of a block would be unknown.
Performance
Measured on an 8-CPU server with a synthetic Llama-3.3-70B-shaped adapter
(80 layers × 7 modules, r = 64, 1.66 GB bf16): adaptersentry scan end to end in
~18–20 s, peak process memory ~160 MB (Python and numpy included). The 1.x engine
needed ~3.3 GB just to load the same file.
Limits — read before relying on a verdict
- Static analysis is a first filter, not a proof. Research on weight-space detection shows strong results in the lab and clear limits: detectors trained on known attacks fail on new ones, and on larger models the variation between honest adapters trained with different random seeds can exceed the signal of a backdoor. A low-risk verdict is not evidence that an adapter is safe; behavioural verification in an isolated environment remains necessary for adapters you do not trust.
- Calibration is partial. The intra-adapter threshold (concentration z ≥ 14) was set on
393 popular PEFT adapters from the Hugging Face Hub (October 2026; mostly Qwen and Llama
up to 14B), presumed benign but not verified: about 5 % of them get
reviewfrom this rule. Larger models and other families may behave differently. Detection was measured only on a toy payload inserted into those adapters (one LoRA direction redirected into 4 outputs of a mid-depthdown_proj): 71 % found when the payload is strong, ≤ 3 % when it is as strong as an ordinary direction. Real backdoors are not toy payloads; a measured detection rate on labelled clean/poisoned adapters and reference profiles per model family are the next steps. Until then the confidence of a verdict is at mostmedium. - An attacker who knows the detector can try to spread a backdoor across many directions and modules, or pad the update with inert components. Static analysis without base-model information cannot fully rule this out.
- Not analysed: full weight matrices (they need the base model), trainable-token deltas;
DoRA adapters are analysed partially. Each of these is reported and lowers the verdict's
confidence or raises it to
review. - Pickle-based formats (
.bin,.pt) are never deserialised. Use a dedicated serialisation scanner for them.
Output: ScanResult 2.1.0
--format json produces one document per adapter: scan (who/when/how), artifact (hashes,
provenance — file name only unless --full-paths), adapter (format, family, rank, α,
scaling, modules), status + coverage (what was and was not analysed), verdict,
anomaly (what the verdict rests on), findings (with exact layer/module/tensor locations),
optional modules, and errors.
- Field reference: docs/output-schema/scan-result.md
- JSON Schema: docs/output-schema/scan-result-2.1.0.schema.json
adaptersentry.load_scan_result()validates a document and rejects other major versions.
Migrating from 1.x
| 1.x | 2.0.0 |
|---|---|
analyze(), scan() → AdapterReport, scan_to_result() |
scan() → ScanResult |
verdict.recommended_action |
verdict.action |
overall_score, ensemble.score (0–100) |
removed — verdict.level + anomaly (z-scores, later p-values) |
status, parse_status, analysis_mode |
status + coverage |
--format summary-json / debug-json |
--format json / full-json |
--fail-on LOW…CRITICAL (finding severity) |
--fail-on review|block (verdict action) |
--rank |
removed — rank is read from the tensors and checked against the config |
adaptersentry-m1 command |
adaptersentry scan |
1.x results are not converted: re-scan the adapters.
Development
git clone https://github.com/nkorvyakov28-AS/adaptersentry-m1
cd adaptersentry-m1
uv sync --frozen --extra dev # or: pip install -e ".[dev]"
pytest tests/ -q
Architecture: docs/architecture/m1-architecture.md · CLI: docs/cli/usage.md · Batch engine: docs/architecture/scan-engine.md
AdapterSentry M1 is the open static-analysis layer. Planned: M2 Behavioral Sandbox — advanced behavioral analysis.
Background
The feature design follows published work on weight-space backdoor detection in LoRA adapters, including Detecting Backdoored LoRAs from Weights Alone (arXiv:2602.15195), Token-Level Generalization in LoRA Adapter Backdoors (arXiv:2605.30189) and Z-PEFT (arXiv:2608.02271). Their results motivate the choices above: shape over magnitude, per-module and per-head features, MLP coverage, and one-class calibration instead of a classifier trained on known attacks.
Security
See SECURITY.md. Report a malicious adapter found in the wild with a GitHub
issue labelled malicious-adapter; report vulnerabilities in AdapterSentry privately.
License
Apache 2.0 — see LICENSE.
Metadata
Release files for adaptersentry 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| adaptersentry-2.0.1.tar.gz | 118.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| adaptersentry-2.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 245.4 kB
Release files / adaptersentry-2.0.1.tar.gz
| Download URL | adaptersentry-2.0.1.tar.gz |
|---|---|
| Size | 118.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
43b3c879b3686f9134f9171854e93042e142986c18da3adc02470f235c0e0e86
|
|
BLAKE2b-256 checksum How to use checksums |
3702c749e73f7304e7eba19f3d2206b2bc700641a6af209fcb539dfcfc9611c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / adaptersentry-2.0.1-py3-none-any.whl
| Download URL | adaptersentry-2.0.1-py3-none-any.whl |
|---|---|
| Size | 127.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a0e7c8a211bf5cf3e0bbb87176d0680153b4ea2f2efa152c84475d0be36571bc
|
|
BLAKE2b-256 checksum How to use checksums |
0213cf08df9641d3255faa73f406168f402489df39be3b1a0438012ad29653a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log