waxseal
English | Tiếng Việt | 中文
Tamper-evident, schema-evolution-safe audit hash chain for AI agent frameworks. Zero dependencies. MIT. Python ≥ 3.11.
waxseal gives your agent a cryptographic audit trail: every action is appended to a SHA-256 hash chain, so any edit, deletion, insertion, or reordering of history is detected. Schema evolution does not trigger false tampering alarms: old rows verify under the fingerprint they were written with.
Why another audit log?
Hash-chained logs break in practice for a boring reason: the schema changes. Two real-world incidents shaped this library:
- A production system widened its hashed field set without a version identity — every historical row failed verification. A mass false tampering alarm.
- An agent-memory tool (beads v1.2.2, 08/2026) accidentally shipped a schema migration; the reverted binary hit "schema version mismatch: database is at v65, binary knows up to v53" and hard-failed. The only escape hatch disabled safety entirely.
Both are the same failure class: ordinal version identity + unknown version treated as an error. waxseal makes that class unrepresentable:
- Envelope design — the chain hashes only a fixed header
(
seq, ts, hash_version, payload_type, payload_hash, prev_hash). Your payload is arbitrary bytes; changing its schema never touches the chain. - Automatic schema fingerprints —
hash_versionis the SHA-256 of a canonical descriptor of the header schema. Widening the field set cannot keep the old identity; old rows always verify under their own fingerprint. - Unknown fingerprint → "unverifiable by name" — never "tampered", never a crash (the RFC 6962 principle: unrecognized types are opaque, not errors). Rollbacks degrade gracefully.
Comparison with other hash-chain approaches
Every hash-chain library detects a flipped byte. These are the things the others don't do (survey of Python audit-log libraries, August 2026 — see DESIGN.md for the literature behind each choice):
| waxseal | typical audit-chain libs | DIY hash chain | |
|---|---|---|---|
| Schema evolution without false tampering alarms (automatic fingerprints) | ✅ | ❌ manual version strings, or none | ❌ |
| Version rollback degrades gracefully (unverifiable ≠ tampered, exit 2 ≠ exit 1) | ✅ | ❌ unknown version = error | ❌ |
Completeness reported separately: dropped_writes, None ≠ 0 |
✅ | ❌ chain-ok implies all-ok | ❌ |
| Fork-proof concurrent appends, with the mechanism documented per backend and a falsifiability-tested lock | ✅ | varies, usually single-writer assumed | ❌ |
| Byte-level SPEC (freeze planned for v1) + golden test vectors → portable to Go/Rust/TS | ✅ | ❌ format = whatever the code does | ❌ |
| Zero runtime dependencies (S3/Postgres clients are injected, never imported) | ✅ | often pulls crypto/serialization stacks | ✅ |
| Redact-before-hash (secrets never reach disk, hash commits to redacted bytes) | ✅ | sometimes | ❌ |
Built-in external anchoring hook (waxseal head) against suffix-rewrite/truncation |
✅ | ❌ | ❌ |
| Forward-secure seals (key-evolving HMAC, stdlib only) + injected Ed25519 signatures | ✅ | ❌ | ❌ |
The first two rows are the failure class from the incidents above; see DESIGN.md for the literature behind each row.
How it works
Data flow — every append:
flowchart LR
A["your agent<br/>append(payload)"] --> R["Redactor<br/>secrets → ***REDACTED***"]
R --> C["canonical bytes<br/>payload_hash = sha256"]
C --> H["EntryHeader built under<br/>the backend's lock<br/>(seq, prev_hash from tail)"]
H --> EH["entry_hash =<br/>sha256(framed header)"]
EH --> B[("backend<br/>JSONL · SQLite · Postgres · S3 · memory")]
EH --> S["attestation sidecar<br/>fs-HMAC seal / Ed25519 signature"]
The chain — why any edit is caught:
flowchart LR
G["genesis<br/>prev_hash = 000…0"] --> E0["entry 0<br/>entry_hash₀"]
E0 -- "prev_hash = entry_hash₀" --> E1["entry 1<br/>entry_hash₁"]
E1 -- "prev_hash = entry_hash₁" --> E2["entry 2<br/>entry_hash₂"]
E2 -. "waxseal head → anchor externally<br/>(OpenTimestamps / RFC 3161 / git)" .-> X["external<br/>trust domain"]
Verification — every outcome is distinct, unknown is never tampered:
flowchart TD
V["waxseal verify"] --> Q1{"seq contiguous?"}
Q1 -- "no" --> X1["BROKEN: seq_gap → exit 1"]
Q1 -- "yes" --> Q2{"prev_hash links?"}
Q2 -- "no" --> X2["BROKEN: prev_hash_mismatch → exit 1"]
Q2 -- "yes" --> Q3{"fingerprint known?"}
Q3 -- "no" --> U["unverifiable by name → exit 2<br/>NOT tampering (rollback-safe)"]
Q3 -- "yes" --> Q4{"entry_hash & payload_hash match?"}
Q4 -- "no" --> X3["BROKEN → exit 1"]
Q4 -- "yes" --> OK["ok → exit 0"]
Install
pip install waxseal
Until the first PyPI release: pip install git+https://github.com/cuongbphv/waxseal
Usage
from waxseal import AuditLog
log = AuditLog.open("~/.myagent/audit/trail.jsonl") # or trail.db for SQLite
log.append(
payload={"tool": "bash", "command": "ls -la", "exit_code": 0},
payload_type="application/vnd.myagent.toolcall+json",
)
result = log.verify()
# VerifyResult(ok=True, checked=1, broken_seq=None, reason=None,
# unverifiable=(), dropped_writes=0)
Redact secrets before they are hashed and stored:
from waxseal.adapters.redactors import RegexRedactor
log = AuditLog.open("trail.jsonl", redactor=RegexRedactor())
log.append(payload={"cmd": "curl -H 'Authorization: Bearer sk-...'"},
payload_type="application/vnd.myagent.toolcall+json")
# cleartext never reaches disk; the hash commits to the redacted payload
CLI:
waxseal verify trail.jsonl # exit 0 intact / 1 broken / 2 unverifiable present / 3 no such trail
waxseal tail trail.jsonl -n 20
waxseal inspect trail.jsonl
waxseal head trail.jsonl # print the chain head (seq + entry_hash) for anchoring
Storage backends
Every backend enforces the same rule: read-tail + append is one critical section, so concurrent writers can never fork the chain.
| Backend | Module | Serialization mechanism | Extra deps |
|---|---|---|---|
| JSONL file | waxseal.adapters.jsonl |
cross-platform file lock | none |
| SQLite | waxseal.adapters.sqlite |
BEGIN IMMEDIATE + PRIMARY KEY(seq) |
none |
| In-memory | waxseal.adapters.memory |
mutex | none |
| Amazon S3 | waxseal.adapters.s3 |
conditional PUT (IfNoneMatch: *) |
inject your boto3 client |
| PostgreSQL | waxseal.adapters.postgres |
pg_advisory_xact_lock + PRIMARY KEY(seq) |
inject your psycopg connection |
# S3 — the client is injected; waxseal itself stays dependency-free
import boto3
from waxseal import AuditLog
from waxseal.adapters.s3 import S3Backend
backend = S3Backend(boto3.client("s3"), bucket="my-audit", prefix="agent-1")
log = AuditLog(backend)
# PostgreSQL — same pattern with a connection factory
import psycopg
from waxseal.adapters.postgres import PostgresBackend
log = AuditLog(PostgresBackend(lambda: psycopg.connect("postgresql://...")))
Note on Kafka: compacted topics delete old records (tombstones), so they are not append-only — do not use them as a tamper-evidence store.
Metadata sources
Beyond agent actions, chain any file/document history:
from waxseal.sources.files import record_file, current_matches_last
record_file(log, "SPEC.md", doc_id="spec") # snapshot content hash into the chain
current_matches_last(log, "SPEC.md", doc_id="spec") # True / False / None (never recorded)
Signatures & forward-secure seals
A keyless hash chain can be recomputed by anyone with write access. The attestation layer closes that gap — without adding a single dependency:
Forward-secure seals (stdlib HMAC, Bellare–Yee / Schneier–Kelsey construction):
the seal key evolves one-way per entry (A_{j+1} = SHA-256(A_j)) and the old key is
discarded, so an attacker who compromises the machine at epoch t cannot forge or
re-seal anything written before t — a consistently rewritten suffix now FAILS
verification instead of passing:
sequenceDiagram
participant W as writer
participant K as sealkey (0600, atomic replace)
participant S as .attest sidecar
W->>K: read A_j
W->>S: seal_j = HMAC-SHA256(A_j, entry_hash_j)
W->>K: A_j+1 = SHA-256(A_j) — A_j is gone
Note over K,S: compromise at epoch t ⇒ seals < t unforgeable
from waxseal import AuditLog
from waxseal.adapters.attest import FileAttestor
from waxseal.domain.sealing import generate_key
k0 = generate_key() # escrow A_0 with your verifier, off this machine
log = AuditLog.open("trail.jsonl",
attestor=FileAttestor("trail.jsonl", initial_key=k0))
log.append(payload={...}, payload_type="application/vnd.myagent.toolcall+json")
log.verify_attestations(initial_key=k0) # AttestResult(ok=True, checked=1, ...)
Real digital signatures (Ed25519 etc.) — the signer is injected, waxseal never imports a crypto library:
# any object with .algorithm, .key_id, .sign(bytes) -> bytes
log = AuditLog.open("trail.jsonl",
attestor=FileAttestor("trail.jsonl", signer=my_ed25519_signer))
log.verify_attestations(verifier=my_ed25519_verifier)
Attestations live in a .attest sidecar (no backend schema changes; old logs stay
readable), and an attestation scheme the verifier doesn't know is reported
unverifiable-by-name — the same never-cry-wolf rule the chain itself follows.
Verification also bakes in the lessons from the systemd-journald FSS CVEs
(2023-31437/38/39): seals are bound to their position in both directions,
cross-checked against hashes recomputed from the trail, and tail truncation of
trail + sidecar together is detected — the keyfile epoch is one-way and cannot be
rolled back.
Limits: Python cannot zeroize memory, and entries written after compromise
are attacker-controlled under any scheme — see DESIGN.md §6.
Integrations
Audit hooks for seven agent frameworks and coding tools. Each one is verified against the target's current hook contract (version noted in its README), records dispatch before execution, redacts secrets before hashing, clips huge outputs visibly, and can never block or veto the host's work — every failure degrades to a labelled, counted dropped write.
Everything ships in the wheel — no source checkout, no file copying:
pip install waxseal
waxseal install hermes # or claude-code / codex / cursor / hermes-gateway
install writes thin shims into the host's config directory (importing
waxseal.integrations.*, so pip install -U waxseal upgrades hook behavior in
place) and prints any settings snippet the host still needs. The LangChain,
CrewAI, and OpenAI Agents integrations need no install step at all — import
them directly, e.g. from waxseal.integrations.langchain import WaxsealCallbackHandler.
| Target | Mechanism | Directory |
|---|---|---|
| Claude Code | hooks (PreToolUse / PostToolUse / UserPromptSubmit) |
integrations/claude-code/ |
| Codex CLI | lifecycle hooks (hooks.json, ≥ 0.149.0) |
integrations/codex/ |
| Cursor | Agent Hooks (.cursor/hooks.json) |
integrations/cursor/ |
| LangChain / LangGraph | BaseCallbackHandler |
integrations/langchain/ |
| CrewAI | event listener (crewai.events) |
integrations/crewai/ |
| OpenAI Agents SDK | RunHooks |
integrations/openai-agents/ |
| hermes-agent | plugin + gateway hook | integrations/hermes/ |
Scope note for the coding tools: these hooks give you a parallel, tamper-evident, secret-free record of every action. They do not (and cannot) rewrite the tool's own transcript files — if a key lands there, rotate it; the waxseal trail is the copy you can keep, share, and verify.
Guarantees and non-guarantees
- Detects: edited entries, deleted entries (seq gap), inserted/reordered entries (prev-hash break), payload substitution.
- Chain integrity ≠ trail completeness: a write dropped before it reaches storage
leaves no gap.
dropped_writesreports this separately;Nonemeans not measured — never conflated with0. - Concurrent writers cannot fork the chain (see backends table).
- waxseal is tamper-evident, not tamper-proof: an attacker with write access can
rewrite the whole suffix of a chain. Use
waxseal headto anchor the head hash in an external trust domain — an OpenTimestamps proof, an RFC 3161 timestamp, or a git commit pushed to a remote — to bound that attack.
Spec & design
- SPEC.md — byte-level format (lp64v1 encoding, PAE-style framing, fingerprint construction; freeze planned for v1) with golden test vectors — portable to any language.
- DESIGN.md — algorithm choices and the academic literature behind them.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file waxseal-0.1.1.tar.gz.
File metadata
- Download URL: waxseal-0.1.1.tar.gz
- Upload date:
- Size: 181.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a9239795dcad0dbf6d21db02f274ad164f7283d3abb8ffd94d7e00c4c307c30
|
|
| MD5 |
ce406804026bcebd2d0ed151b06a184b
|
|
| BLAKE2b-256 |
7d67fe65b90bce239b72571f6897ae5eb9d37147124639e5da87edd655a79c07
|
Provenance
The following attestation bundles were made for waxseal-0.1.1.tar.gz:
Publisher:
release.yml on cuongbphv/waxseal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
waxseal-0.1.1.tar.gz -
Subject digest:
3a9239795dcad0dbf6d21db02f274ad164f7283d3abb8ffd94d7e00c4c307c30 - Sigstore transparency entry: 2550595362
- Sigstore integration time:
-
Permalink:
cuongbphv/waxseal@7481a4843a070661c49731ea4a8b2de192c35780 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/cuongbphv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7481a4843a070661c49731ea4a8b2de192c35780 -
Trigger Event:
release
-
Statement type:
File details
Details for the file waxseal-0.1.1-py3-none-any.whl.
File metadata
- Download URL: waxseal-0.1.1-py3-none-any.whl
- Upload date:
- Size: 61.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
931b5192a367b09540a4329d5efd06f6871dd6e504da74dc625976df8da96a6e
|
|
| MD5 |
a0d180cb4f93130ee9a16119edfff6b2
|
|
| BLAKE2b-256 |
3cc6a00e279b7f7ec4563a436e824b1a1a84c23f86790a58e8d654e5b7ed5dad
|
Provenance
The following attestation bundles were made for waxseal-0.1.1-py3-none-any.whl:
Publisher:
release.yml on cuongbphv/waxseal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
waxseal-0.1.1-py3-none-any.whl -
Subject digest:
931b5192a367b09540a4329d5efd06f6871dd6e504da74dc625976df8da96a6e - Sigstore transparency entry: 2550595501
- Sigstore integration time:
-
Permalink:
cuongbphv/waxseal@7481a4843a070661c49731ea4a8b2de192c35780 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/cuongbphv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7481a4843a070661c49731ea4a8b2de192c35780 -
Trigger Event:
release
-
Statement type: