@@ -134,8 +134,35 @@ def _log(message: str):
134134
135135
136136def _output (data : dict ):
137- """Print JSON to stdout with consistent formatting (pretty-printed)."""
138- print (json .dumps (data , indent = 2 , ensure_ascii = False ))
137+ """Print JSON to stdout without importing modules that may redirect streams.
138+
139+ If mempalace.mcp_server is already loaded, reuse its saved real stdout fd.
140+ Otherwise, write directly to fd 1 so hook responses still go to stdout even
141+ if sys.stdout has been redirected elsewhere.
142+ """
143+ payload = (json .dumps (data , indent = 2 , ensure_ascii = False ) + "\n " ).encode ("utf-8" )
144+
145+ real_stdout_fd : int | None = None
146+ mcp_mod = sys .modules .get ("mempalace.mcp_server" ) or sys .modules .get (
147+ f"{ __package__ } .mcp_server" if __package__ else "mcp_server"
148+ )
149+ if mcp_mod is not None :
150+ real_stdout_fd = getattr (mcp_mod , "_REAL_STDOUT_FD" , None )
151+
152+ fd = real_stdout_fd if real_stdout_fd is not None else 1
153+ offset = 0
154+ try :
155+ while offset < len (payload ):
156+ try :
157+ offset += os .write (fd , payload [offset :])
158+ except InterruptedError :
159+ continue
160+ return
161+ except OSError :
162+ pass
163+
164+ sys .stdout .buffer .write (payload )
165+ sys .stdout .buffer .flush ()
139166
140167
141168def _get_mine_dir (transcript_path : str = "" ) -> str :
@@ -259,10 +286,29 @@ def hook_stop(data: dict, harness: str):
259286 stop_hook_active = parsed ["stop_hook_active" ]
260287 transcript_path = parsed ["transcript_path" ]
261288
262- # If already in a save cycle, let through (infinite-loop prevention)
289+ # If already in a block-mode save cycle, let through (infinite-loop prevention).
290+ # Silent mode saves directly without returning {"decision":"block"}, so there's
291+ # no loop to prevent — and Claude Code's plugin dispatch sets this flag on every
292+ # fire after the first, which would otherwise suppress all subsequent auto-saves.
263293 if str (stop_hook_active ).lower () in ("true" , "1" , "yes" ):
264- _output ({})
265- return
294+ # Safe default: assume silent mode on any config-read failure so saves
295+ # proceed rather than being silently dropped. Silent mode is the default
296+ # (v3.3.0+), so if we can't read config, behave as if it's still on.
297+ silent_guard = True
298+ try :
299+ from .config import MempalaceConfig
300+ except ImportError as exc :
301+ _log (
302+ f"WARNING: could not import MempalaceConfig for stop guard: { exc } ; defaulting to silent mode"
303+ )
304+ else :
305+ try :
306+ silent_guard = MempalaceConfig ().hook_silent_save
307+ except AttributeError as exc :
308+ _log (f"WARNING: could not read hook_silent_save: { exc } ; defaulting to silent mode" )
309+ if not silent_guard :
310+ _output ({})
311+ return
266312
267313 # Count human messages
268314 exchange_count = _count_human_messages (transcript_path )
0 commit comments