NEXO 7.9.34 — Email parser fix + Guardian hook hardening
Published 2026-04-26. Patch release over v7.9.33.
Why the inbox went quiet
NERO — the email-automation agent on top of NEXO Brain — stopped replying to a subset of emails over the last few days. The pattern was always the same: senders with non-ASCII display names or Q-encoded subjects (utf-8 / quoted-printable, e.g. =?utf-8?q?Confirmaci=C3=B3n?=) landed in the IMAP UNSEEN scan, the worker tried to parse the headers via _parse_email_headers, and the email vanished without trace. The DB had no row, the inbox kept growing, and Nero never spoke up.
Root cause: email.message_from_bytes(...).get("Message-ID") in the Python stdlib returns an email.header.Header instance — not a plain string — when the underlying header is Q-encoded. .strip() on a Header raises TypeError: 'Header' object is not subscriptable; the parser caught the exception, logged it at DEBUG, and returned {}. The downstream worker treated an empty dict as "skip this email" and moved on. The DEBUG log meant the operator never saw the failure.
v7.9.34 routes every msg.get(...) through _decode_header, which decodes Q-encoding AND coerces to str — so Message-ID, In-Reply-To, References, From, Subject, and Date all come out as plain strings even when the wire form is Q-encoded. The exception log is lifted from DEBUG to WARNING, so a future regression cannot drop emails silently again. 5 new unit tests in tests/test_email_monitor_parser.py pin both the Q-encoded round-trip and the visibility regression.
Why terminal Claude was ignoring the Guardian
The PreToolUse Guardian gate (Block K — G3 destructive, G3 SSH remote write, G4 conditioned-file guard) emits a JSON permissionDecision: deny response when a hard-mode rule fires. That JSON is the documented Claude Code contract for blocking a tool call. In Claude Desktop the deny is honoured cleanly. In terminal Claude Code we observed cases where the model proceeded with the next tool call anyway — the JSON deny was being dropped or out-of-order delivered mid-tool-loop, and the hook was exiting with code 0.
v7.9.34 keeps the JSON deny as the primary contract but, on a hard block, also writes the structured Guardian reason to stderr and exits with code 2 — the documented PreToolUse blocking exit. Belt-and-suspenders: the model receives the same Guardian reason through both channels, the process-exit layer enforces the block even if the JSON branch is lost, and the model self-corrects instead of blindly retrying. Shadow mode and gate-off paths still exit 0 (no behaviour change).
Verification
97 tests green across email_monitor_parser (5 new), email_monitor_checkpoints, pre_tool_use_hook (2 hardened assertions), call_model_raw, call_model_raw_overrides, call_model_raw_overrides_e2e, agent_runner_override_env, fase4_lint_baseline, and security_baseline. Bandit clean (python3 -m bandit -r src/ --severity-level high --confidence-level high). Ruff clean.