Honest scope: inspeximus has had a first internal security pass (2026-07-13), not a formal third-party pentest. This document states what inspeximus defends, what it does not, and the known residual footguns — so you can decide what to rely on. If you find an issue, open a GitHub issue or security advisory.
inspeximus is an in-process Python library plus an optional stdio MCP server (inspeximus-mcp). It is not a
network service and does not open sockets (the MCP server speaks over stdio and trusts the host that launched
it). The realistic adversary is therefore the content you store — a memory written from an untrusted user
conversation, a poisoned document, a compromised tool result — not a remote attacker hitting an endpoint.
That content threat is the thing inspeximus is actually built to address: keyed supersession, echo_guard,
retract_lineage, the corroboration-gated influence path (recall(influence_only=True)), the authorized-revert
channel, and the tamper-evident write receipts. See the README and probes/ for the measured behaviour and its
honest limits.
- No remote-code-execution surface. The store is JSON only — no
pickle,eval,exec,yaml.load,subprocess, oros.systemanywhere. Loading an untrusted store file cannot execute code. - No ReDoS from content. The router regexes are fixed, module-level, and use bounded quantifiers; any
user-derived string used in a pattern is
re.escaped. - Constant-time secret comparison. Revert capabilities (HMAC) are checked with
hmac.compare_digest; signatures use Ed25519.
-
verify_writes()withoutexpected_pubkeyis not operator-adversarial-safe. A signed receipt carries its ownpubkey; the signature is verified against that self-contained key. An attacker who can rewrite the store file can replace the signature and the pubkey with their own keypair, andverify_writes()will pass. To detect a rewrite you must either pinexpected_pubkey(verify_writes(expected_pubkey=...)) or, against a key-holder who can re-sign, use the externalanchor()/verify_consistency()(a Certificate-Transparency-style witness — see the governance section). The bareverify_writes()proves only internal chain consistency, not authenticity. -
No resource limits by default. A single
remember()accepts arbitrarily large text, and the active set grows unbounded unless you setcapacity=. Malicious or runaway content can exhaust memory/CPU (recall()scales with the active set). If you ingest untrusted content, setcapacity=and cap input size at the application boundary. (A future minor release may add an opt-in per-record size guard.)But
capacity=is not a clean mitigation — it converts a growth attack into a TARGETED DELETE. Eviction ranks byvalue, which is caller-supplied and unbounded, and the two-tier policy protects the topprotect_fracby raw value — exactly what an attacker buys. Measured: withcapacity=10, fifty writes atvalue=1000.0evicted 5 of 5 victim records held atvalue=1.0. So if writes come from somewhere untrusted, capping the store size hands the attacker a way to delete specific memories, which may be worse than the exhaustion it prevents. Clampvalueat your application boundary too, or keep untrusted writes in a separate store. -
Erasure attests the act, not physical destruction.
forget_subject()/ the deletion manifest / the erasure auditor make deletion tamper-evident and cross-store-auditable; they do not prove bytes were destroyed, and a retained embedding elsewhere can reconstruct content (see the erasure auditor + README). The compliant fix for a leaking store is hard-delete + reindex or crypto-shredding.
The single-writer guard (StoreChangedOnDisk) shipped in 1.67.0. It fires when a handle is about to
save over a file that changed underneath it. A handle from an OLDER release does not have it and saves
anyway, erasing whatever the newer one wrote.
Measured, 1.51.0 alongside 1.69.0 on one store file:
1.51.0 opens a handle -> 1.69.0 writes and flushes -> 1.51.0 flushes
final store: ['baseline record', 'written by OLD after the fact']
the 1.69.0 record is GONE, no error, no warning
same interleave, both handles on 1.69.0:
final store: ['baseline record', 'written by NEW while OLD held a handle']
the guard refuses the clobber
This cannot be fixed from our side: 1.51.0 is published and has no guard. Do not let processes on different inspeximus versions share a store file — pin one version per store, or give each writer its own file. Upgrading every writer at once removes the exposure; upgrading one of them does not, and the partially-upgraded state is the dangerous one.
The guard is an enforcement, not a solution, even between matching versions: two writers are told about
the conflict and reload() merges them, but they still cannot proceed concurrently. A real fix is a lock
or a different storage format.
Open a GitHub issue (or a private security advisory) on https://github.com/DanceNitra/inspeximus. This is an open-source project maintained on a best-effort basis; there is no formal SLA.