fix(hooks): write hook JSON to real stdout, bypassing mcp_server redirect

mempalace.mcp_server redirects stdout → stderr at module-level import
(both Python-level and fd-level via os.dup2) to protect the MCP stdio
protocol from ChromaDB's C-level noise. Silent-save imports mcp_server
transitively via _save_diary_direct, so by the time _output() calls
print(), sys.stdout is actually stderr.

Claude Code reads hook output from fd 1. With the redirect in effect,
fd 1 points to fd 2, so our {"systemMessage": "✦ N memories woven..."}
JSON lands on stderr and Claude Code never renders it. The save still
happens, the marker still advances — the user just never sees the
beautiful checkpoint notification in their terminal.

Fix: _output() now writes to _REAL_STDOUT_FD (saved by mcp_server before
the redirect) via os.write(), falling back to sys.stdout only when the
saved fd is unavailable (e.g., hooks_cli imported without mcp_server).

Test: bash hook script 2>/dev/null now shows only the JSON;
2>&1 >/dev/null shows only the Diary entry log line — clean separation
restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jp
2026-04-18 14:45:19 -07:00
parent 914945637c
commit 6a3a5c7a3d
+18 -2
View File
@@ -134,8 +134,24 @@ def _log(message: str):
def _output(data: dict):
"""Print JSON to stdout with consistent formatting (pretty-printed)."""
print(json.dumps(data, indent=2, ensure_ascii=False))
"""Print JSON to the real stdout, even if mcp_server has hijacked sys.stdout.
mempalace.mcp_server redirects stdout → stderr at module import (fd and
sys-level) to protect the MCP stdio protocol from ChromaDB's C-level
prints. Silent-save imports it transitively via _save_diary_direct, so
sys.stdout is stderr by the time we get here. Claude Code reads hook
output from fd 1, so we write there directly using the saved fd.
"""
payload = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
try:
from .mcp_server import _REAL_STDOUT_FD
if _REAL_STDOUT_FD is not None:
os.write(_REAL_STDOUT_FD, payload.encode("utf-8"))
return
except Exception:
pass
sys.stdout.write(payload)
sys.stdout.flush()
def _get_mine_dir(transcript_path: str = "") -> str: