v5.4.1 — Hook hygiene fix: the Sensory Register was silently blinded

This release is a post-mortem first and a patch second. Francisco noticed during a normal working session that I (the agent) was not using NEXO's protocol tools as disciplined as expected. His hypothesis: "there might be a hook bug". He was right, and it was worse than either of us assumed.

What was broken

The PostToolUse hook capture-session.sh has been writing "tool":"unknown" to the Sensory Register (~/.nexo/brain/session_buffer.jsonl) since the hook was introduced on 2026-04-12. Across two full days that buffer accumulated 7,052 lines — and 100% of them were noise. A grep -v unknown against the buffer returned zero real tool calls.

Root cause

The hook read $CLAUDE_TOOL_NAME — an environment variable that Claude Code has never set. Claude Code passes the tool name through a JSON payload on stdin, which the hook was reading with cat but never parsing. A follow-up commit titled "harden all hooks against empty stdin" hardened the pipe but never extracted the data, so the bug was entrenched behind a plausible-looking defensive change.

The result: two days of silent blindness. No error, no warning, no failing test. Just a fast-growing file full of nothing.

The fix

Parse the JSON. Same pattern as capture-tool-logs.sh, which has been correct all along:

TOOL_NAME=$(echo "$INPUT" \
    | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" \
    || true)
[ -z "$TOOL_NAME" ] && exit 0

An empty tool name now exits silently instead of writing "unknown" to the buffer. The filter that skipped high-frequency read-only tools (Read, Glob, Grep, LS, Skill, ToolSearch, TodoWrite) stays, but Bash is no longer filtered — that was the other half of the bug, because Bash is where commits, pushes, npm publishes, and infra changes live.

Related contamination

While auditing the hooks directory, a ~/.nexo/hooks/capture-session 2.sh duplicate was discovered. The v5.3.29 release hygiene gates block these files in the repo tree, but this one lived in the runtime bucket (~/.nexo/hooks/), where the gates do not reach. It has been removed in v5.4.1 and the hook fix supersedes both copies.

On upgrade

nexo update strips pre-existing "tool":"unknown" lines from your session_buffer.jsonl once, with a .pre-v5.4.1.bak backup kept alongside so nothing is lost unrecoverably. Fresh entries start showing real tool names (Bash, Edit, Write, mcp__nexo__nexo_heartbeat, and so on) immediately.

Lesson

Release-readiness gates protect the source tree. Runtime hygiene gates need to reach the runtime too. And hooks that write non-trivial data deserve a test that at least asserts the data is not a constant.

Update

npm update -g nexo-brain
nexo update