Skip to main content

AdapterSentry

Python License Version

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
  1. Validate before reading. The file must be a regular .safetensors file; the header length, dtype, shape, byte ranges, element counts and LoRA rank are checked from the header before any tensor is allocated.
  2. 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.
  3. 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 — for lm_head and embeddings, on a few tokens.
  4. Compare within the adapter. Each module is compared with its family (all q_proj, all down_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.
  5. 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 declared r (typical of merged adapters), code/pickle/archive files next to the adapter.
  6. 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 review from 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-depth down_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 most medium.
  • 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.


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

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for adaptersentry 2.0.2
File Size Uploaded
adaptersentry-2.0.2.tar.gz 119.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for adaptersentry 2.0.2
File Interpreter ABI Platform
adaptersentry-2.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 247.2 kB

Release files / adaptersentry-2.0.2.tar.gz

Download URL adaptersentry-2.0.2.tar.gz
Size 119.1 kB
Tags Source
SHA-256 checksum
How to use checksums
f1987991936acf235dbebc2544866b2de4ea5e24d242881897376cd96515f89d
BLAKE2b-256 checksum
How to use checksums
8fffb85cc1e1bcd58b3f1cc7d43a75373ddfbeeb98874cdcee1209d957ff6ca1
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

Release files / adaptersentry-2.0.2-py3-none-any.whl

Download URL adaptersentry-2.0.2-py3-none-any.whl
Size 128.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
33754f4541f18b1f9cba087c7a4a5bf25993c991705b48ce952ee277c7aa1115
BLAKE2b-256 checksum
How to use checksums
57ada1f86c469a7e33033e6d0488a4fb1eeb0e4af44d45bb4f19146afce0ce0e
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

Release history Release notifications | RSS feed

This release

2.0.2 This release

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.0.3

2 release files

1.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page