From 6a3a5c7a3d5ea305ff725d49ceab515e8ba46c9e Mon Sep 17 00:00:00 2001 From: jp Date: Sat, 18 Apr 2026 14:45:19 -0700 Subject: [PATCH] fix(hooks): write hook JSON to real stdout, bypassing mcp_server redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- mempalace/hooks_cli.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/mempalace/hooks_cli.py b/mempalace/hooks_cli.py index 61e5a9c..c20a6c6 100644 --- a/mempalace/hooks_cli.py +++ b/mempalace/hooks_cli.py @@ -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: