Skip to main content

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:

  1. 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.
  2. Automatic schema fingerprintshash_version is 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.
  3. 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, None0 ❌ 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_writes reports this separately; None means not measured — never conflated with 0.
  • 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 head to 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

waxseal-0.1.0.tar.gz (179.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

waxseal-0.1.0-py3-none-any.whl (60.9 kB view details)

Uploaded Python 3

File details

Details for the file waxseal-0.1.0.tar.gz.

File metadata

  • Download URL: waxseal-0.1.0.tar.gz
  • Upload date:
  • Size: 179.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for waxseal-0.1.0.tar.gz
Algorithm Hash digest
SHA256 bf70233df1f0ee1a01572b1eabc5f5a0574d128ff62d02c23e34a318b214be4c
MD5 49e75fce92f9d153bd9ace5eec111d8f
BLAKE2b-256 7db0bb2c963e0755ec204bb4d5d6a7930ae2572bc73e487c4184e71f24904c17

See more details on using hashes here.

Provenance

The following attestation bundles were made for waxseal-0.1.0.tar.gz:

Publisher: release.yml on cuongbphv/waxseal

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file waxseal-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: waxseal-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 60.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for waxseal-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7c7e4abeb191b75d590dfc8f4bad61af05941468679e32127e3d670be3d65a42
MD5 86c6f79f29a48fc8f74af414280ff5be
BLAKE2b-256 d8bc878791368b4c739813d254a1a3361913ca07f058e1dcc726b8491c18a63f

See more details on using hashes here.

Provenance

The following attestation bundles were made for waxseal-0.1.0-py3-none-any.whl:

Publisher: release.yml on cuongbphv/waxseal

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page