Build1 publisher3 min readPublished
trelix's most useful release detail is an exit code, not the audit log
Six new feature areas landed in v3.0.0, every one off by default, and a verify command that refuses to call an unreadable database clean.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- The span from trelix v2.11.0 to v3.1.1 is six releases (v2.11.1, v2.12.0, v3.0.0, v3.0.1, v3.1.0, v3.1.1), the last dated 2026-08-15.
- That span comprises 68 commits, 137 files changed, +19,829/-1,211 lines.
- Pointed at something SQLite cannot open, trelix audit verify exits 2 rather than 0, because 'I could not check' and 'I checked and it is clean' must never collapse into the same green build.
- After seeding three events and running sqlite3 audit.db "UPDATE audit_log SET principal='attacker' WHERE id=2", trelix audit verify --db audit.db reports the audit chain as TAMPERED with first divergent entry id 2, and exits with code 1.
- Deleting the newest row instead is still caught, naming id 3, even though the surviving rows form a perfectly valid chain.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
trelix closed a six-release run with v3.1.1, dated 2026-08-15: 68 commits, 137 files changed, +19,829/-1,211 lines since v2.11.0 [1][2]. The write-up on dev.to leads with a hash-chained audit trail, but the more transferable decision is smaller than any feature in the release: `trelix audit verify` exits 2 when it cannot open the database, rather than 0 [3].
The demonstration is short. Seed three events, reach past the application with `sqlite3 audit.db "UPDATE audit_log SET principal='attacker' WHERE id=2"`, and verify reports the chain as tampered, names id 2 as the first divergent entry, and exits 1 [4]. Delete the newest row instead and it names id 3, even though the surviving rows form a valid chain [5]. Point it at a file SQLite cannot open and it exits 2 [3]. That is three outcomes where most check commands ship two, and the third is the one that keeps a broken check from being reported as a passing one [6].
The truncation catch needs its own machinery. Each row's `entry_hash` is `sha256(prev_hash || canonical_json(content))`, canonicalised with `sort_keys=True` and `separators=(",", ":")` [7]. Exactly eleven columns are hashed, in a fixed `_CONTENT_COLUMNS` tuple, and the DB-assigned `id` is deliberately excluded because a writer cannot hash a value it does not have until after the INSERT [8]. A hash chain is therefore structurally blind to a deleted tail [9], so an `audit_meta` table carries a running `count` and `head_hash`, upserted inside the same transaction as the insert, and `verify_chain` checks both against the chain it just walked [10]. `audit_log.id` is `INTEGER PRIMARY KEY AUTOINCREMENT` rather than a rowid alias, so a delete-then-refill cannot close the gap it made [11].
The trail is a separate SQLite file, defaulting to `<cwd>/.trelix/audit.db` and never the index database, on the grounds that the index is disposable and a trail that vanishes on reindex is not a trail [12]. It is stdlib `sqlite3`, 54 tests, and fail-open by default via `AuditConfig.fail_closed: bool = False` so a full disk is not an outage [13]. `trelix audit export --format ndjson` writes one JSON object per line for SIEM ingestion [14].
The discipline that costs more is the defaults. v3.0.0 carries six new feature areas plus an opt-in FTS5 declaration boost [15], and all of them ship off: `TRELIX_AUDIT_ENABLED`, `TRELIX_OIDC_ENABLED`, `TRELIX_LLM_THINKING_ENABLED` and `TRELIX_RETRIEVAL_COMPRESSION` all false, `declaration_boost_enabled` false, and `context_token_budget` still the same `12_000` integer as v2.12.0 [16]. A default v3.0.0 install assembles context byte-identically to a default v2.12.0 install, and there is a test asserting it rather than a release note claiming it [17]. The major bump is scope, not breakage [18].
The SSO half is scoped the same way: trelix is a resource server, not an OIDC client, with no `/login`, no `/callback`, no code exchange and no refresh [19]. Algorithms are asymmetric-only, defaulting to RS256 and ES256, and the check runs in `OidcVerifier.__init__`, again against the unverified JWS header in `authenticate`, and a third time in `jwt.decode` [20]. The test worth reading skips trelix's own header gate and signs a token with HS256 using a real 2048-bit public key's PEM as the HMAC secret, then asserts `jwt.decode` alone refuses it [21]. `Principal.principal_id` is subject plus issuer, never email, because email-keyed identity is an account-takeover primitive [22].
Two things to watch. Fail-open is the seam: because a dropped write never reaches the insert transaction that increments `audit_meta.count`, an event lost to a full disk leaves the chain and the count mutually consistent, so verify will call it clean [23]. And defaults tend to drift after the release that announces them, so the number worth rechecking at v3.2 is whether `context_token_budget` is still 12,000 and whether the four false flags are still false [16].