NEXO 6.0.5: Strict pre-tool guardrail stops emitting “unknown target”
Published April 17, 2026 · by Francisco Cerdà Puigserver
The bug that shipped in 6.0.2 and survived until 6.0.4
Several Claude Code builds deliver PreToolUse without a session_id field. Until 6.0.5, src/hook_guardrails.py::process_pre_tool_event consulted only payload["session_id"]. With no id, _resolve_nexo_sid returned an empty string, the strict branch recorded a strict_protocol_write_without_startup debt, and the formatter emitted:
NEXO STRICT MODE BLOCKED THIS EDIT:
- Start the shared-brain session first: call `nexo_startup`, then `nexo_task_open`, before editing (unknown target).
Operators who had already called nexo_startup, opened a protocol task, acknowledged guard warnings and tracked the target file still saw the block on every Edit/Write. Tracked as learning #411. 6.0.3 shipped a partial fix for the handle_guard_check side of the same family but did not cover the missing-payload case for edits.
The fix
The SessionStart hook already writes the active Claude Code session UUID to $NEXO_HOME/coordination/.claude-session-id. 6.0.5 adds a fallback helper that reads it:
claude_sid = str(payload.get("session_id", "") or "").strip()
if not claude_sid:
claude_sid = _read_claude_session_id_from_coordination()
sid = _resolve_nexo_sid(conn, claude_sid)
The lookup walks $NEXO_HOME/coordination/.claude-session-id, then ~/.nexo/coordination/.claude-session-id, and stops at the first non-empty value. Fail-closed is preserved: if neither the payload nor the file yields an id, the guardrail still blocks with missing_startup. No behavioural change for callers that already supply session_id.
Tests that silently regressed since 6.0.2
Three pre-tool tests had been failing quietly:
test_process_pre_tool_event_allows_public_contribution_checkoutassertedresult["skipped"] is Truewithreason="lenient mode". After the public-contribution refactor, that mode only disables the live-repo block — strict discipline still applies. The test now asserts the specific property it was designed to guard (noautomation_live_repo_write_blockeddebt, noautomation_live_reporeason code) and creates the protocol task the strict path expects.test_process_pre_tool_event_does_not_treat_runtime_home_as_live_repo_when_not_git_checkouthad the same stale shape. Fixed the same way.test_non_tty_returns_lenientinheritedNEXO_INTERACTIVE=1from the parent shell (NEXO Desktop orclaude’s interactive launcher both export it) and therefore read strict even when_force_tty(False). The helper nowmonkeypatch.delenvsNEXO_INTERACTIVEso the TTY signal is the only thing steering strictness.
pytest joins CI
The real reason those regressions shipped is that CI only ran ruff, bandit, verify_release_readiness and verify_client_parity. None of those execute the test suite. 6.0.5 adds .github/workflows/tests.yml which runs pytest tests/ -q --maxfail=5 on every PR and push to main. Merges are blocked on failure.
Merged alongside
- Legacy Python Claude hooks purge (commit 9e42b03). Removes obsolete hook handlers on
nexo update. - macOS test/runtime isolation hardening (commit 6005288). Smoke installs on macOS no longer touch real launchd; tests run in an isolated launchd namespace so a developer laptop can never have
nexo installclobber its live LaunchAgents.
Two open tails
Two tests/test_protocol.py cases assert a handle_task_close / cognitive-trigger shape that no longer exists. Marked pytest.mark.xfail(strict=False) for 6.0.5 so the new gate stays green and tracked in followup NF-TEST-PROTOCOL-API-REFACTOR.
Suite
1093 passed, 2 xfailed, 1 skipped.