v0.6.3 closed 14 security findings across the stack. v0.6.4 is the cleanup pass - 12 smaller items that the v0.6.3 review explicitly parked for the next cycle. Nothing as dramatic as closing the SSRF redirect bypass, but each one tightens a real edge case.
And then a production install crashed immediately after publish. More on that in a moment.
What shipped
Timing-safe audit chain verification
verify_chain was comparing SHA-256 hashes with plain != / !==. For any compliance deployment that exposes verify_audit over a public API, that's a timing side-channel. Fixed with hmac.compare_digest in Python and crypto.timingSafeEqual with a length pre-check in JS.
Single source of truth for the audit entry schema
The B3 allow-list validator that guards audit log writes was a hand-written frozenset - maintained independently of the AuditEntry dataclass. Add a field to the dataclass without updating the frozenset, and writes silently fail. Add to the frozenset without updating the dataclass, and you get a TypeError at log time.
# Before
_ENTRY_ALLOWED_KEYS = frozenset({
"timestamp", "entry_hash", "prev_hash", ... # maintained by hand
})
# After
_ENTRY_ALLOWED_KEYS = frozenset(f.name for f in dataclasses.fields(AuditEntry))
One line. No more drift hazard.
MCP server log hygiene
Seven exception handlers in the MCP server were logging str(e) on every error. In normal operation, that's fine - but if a cloakllm-py exception ever includes PII context in its message (a malformed input, a validation detail), it lands in the operator's server log, violating the no-PII invariant through a side door.
The fix: exception handlers now log only type(e).__name__ by default. Set CLOAKLLM_DEBUG=1 to get the full exception text when you actually need it for debugging.
Uniform error returns from MCP tools
All MCP tool handlers declare -> dict. Eleven validation error paths were returning json.dumps(err) - a string, not a dict. Callers doing isinstance(result, dict) would silently pass string errors through. Fixed across the board; all error paths now return consistent {"error": "..."} dicts.
JS middleware idle-refresh TTL
The JavaScript middleware was evicting token maps based on creation time rather than last-use time. A multi-turn conversation running longer than 5 minutes would have its token map evicted mid-session - tokens would fail to desanitize in later turns. TTL semantics now match the MCP server: the clock resets on each use.
O(1) LRU eviction in the MCP token map
The MCP server's _TOKEN_MAPS store was evicting the oldest entry by scanning all entries with min() under a lock. Fine at small scale, but at 10,000 concurrent token maps that's a noticeable pause. Replaced with collections.OrderedDict and popitem(last=False).
Everything else
Object.create(null) accumulator in _legacyCanonicalJson - the existing prototype-key filter already does the work; this removes the footgun
- JS
verifyChain JSDoc updated with cross-SDK asymmetry note (Python v0.6.0 chains with non-ASCII fail silently without it)
- TypeScript declarations for
auditStrictChain, auditStrictPaths, and verifyChain.legacyCanonical? - all added in v0.6.3 source but missing from index.d.ts
litellm_middleware.disable() uses explicit is not None guard
- Test isolation flake fixed via
conftest.py autouse fixture that resets module-level warning gates between tests
The hotfix
After tagging v0.6.4 and watching all three publish workflows go green, I did a clean-room install:
pip install cloakllm-mcp==0.6.4
python -c "import server"
TypeError: FastMCP.__init__() got an unexpected keyword argument 'description'
Every fresh install of cloakllm-mcp - not just v0.6.4, but v0.6.3 and v0.6.2 too - was broken at import. The upstream mcp[cli] package had renamed FastMCP(description=...) to instructions= in a recent release. Our server.py was still passing description=.
CI hadn't caught it. The reason: test_server.py uses a _FakeFastMCP mock that accepts **kwargs - which is the correct policy for unit tests focused on tool behavior, but a blind spot for upstream API changes. The mock accepted the old kwarg; the real package didn't.
cloakllm-mcp 0.6.4.post1 ships the one-line fix. pip auto-prefers post-releases for all users, so pip install -U cloakllm-mcp will pull it automatically.
The lesson: a green CI pipeline proves your tests pass. It doesn't prove the package works on a fresh install. v0.6.5 adds a CI step that installs the just-built wheel into a separate clean venv and runs python -c "import server" before the publish step. Simple, and now mandatory.
Install
pip install -U cloakllm cloakllm-mcp
npm install cloakllm@latest
Drop-in safe from v0.6.3. Full changelog in each repo.
Both releases - v0.6.4 and the v0.6.4.post1 hotfix - published via OIDC trusted publishing with auto-generated provenance attestations. No long-lived credentials in CI.