Summary
In Claude Code 2.1.114, Stop hooks dispatched from a plugin (hooks.json) no longer render their {"systemMessage": "..."} JSON output as a visible <Line> in the terminal, even though the hook script exits 0 and outputs clean JSON to stdout.
Settings.local.json-registered Stop hooks appear to have broken more thoroughly in 2.1.114 (no invocation at all in some sessions), which led me to migrate the hook to a plugin. The plugin-scope dispatch does invoke the hook, it does read its stdout, and the subsequent stop_hook_active:true handling works — but the systemMessage no longer surfaces to the user.
Environment
- Claude Code:
2.1.114 (Claude Code) (binary symlinked from ~/.local/bin/claude)
- OS: Ubuntu (Linux 6.17)
- Plugin: MemPalace fork — registered
Stop + PreCompact via plugin hooks.json
- Hook script:
bash ${CLAUDE_PLUGIN_ROOT}/hooks/mempal-stop-hook.sh → invokes python -m mempalace hook run --hook stop --harness claude-code
What I observe
The hook runs. The hook saves data. The hook prints JSON to fd 1 (stdout), separate from any log noise on fd 2 (stderr). Verified by running the exact script Claude Code dispatches:
$ echo '{"session_id":"...","transcript_path":"...","hook_event_name":"Stop","stop_hook_active":true,"cwd":"..."}' \
| bash ~/.claude/plugins/cache/mempalace/mempalace/3.3.1/hooks/mempal-stop-hook.sh > out 2> err
$ cat out
{
"continue": true,
"suppressOutput": false,
"systemMessage": "✦ 30 memories woven into the palace — stop, hook, readme"
}
$ cat err
Diary entry: diary_memorypalace_20260418_... → memorypalace/diary/checkpoint
But in the terminal UI, nothing shows — not the Stop says: line, not the ✦, not even a collapsed ● Ran N stop hooks summary. Silent success on the UI side even though the producer side is clean.
Expected behavior
Per prior documentation and my own prior working session (2026-04-10 through 2026-04-17, pre-upgrade), a Stop hook outputting {"systemMessage": "✦ ..."} renders as a visible single-line <Line> element via AttachmentMessage.tsx handling hook_system_message attachments. Described as: "silent success is invisible by design — but the systemMessage Line is the one visible signal."
What I tried
- Confirmed the hook fires (diagnostic probe file at
/tmp/stop-hook-probe shows Claude Code invoking the script).
- Confirmed stdout contains clean JSON (not mixed with stderr log lines).
- Added
continue: true, suppressOutput: false alongside systemMessage — no change in rendering.
- Marker advances and diary drawers are written — the save path is healthy.
So the regression appears to be on the UI side: either AttachmentMessage.tsx no longer emits the hook_system_message <Line> for plugin-dispatched Stop hooks, or the attachment is being suppressed somewhere between hook-output capture and render.
Repro (minimal)
Any plugin with a Stop hook that outputs {"systemMessage": "test"} to stdout. Expected: a visible Stop says: test line. Actual: nothing visible, even though save-side side effects succeed.
Impact
Any plugin using silent-save patterns (non-blocking, direct data writes with user-visible confirmation via systemMessage) loses its only visibility signal. The pattern is deliberately non-blocking by design; systemMessage was the UX bridge. Without rendering, users can't tell anything happened.
Additional context
Happy to provide additional traces if helpful.
Summary
In Claude Code 2.1.114, Stop hooks dispatched from a plugin (
hooks.json) no longer render their{"systemMessage": "..."}JSON output as a visible<Line>in the terminal, even though the hook script exits 0 and outputs clean JSON to stdout.Settings.local.json-registered Stop hooks appear to have broken more thoroughly in 2.1.114 (no invocation at all in some sessions), which led me to migrate the hook to a plugin. The plugin-scope dispatch does invoke the hook, it does read its stdout, and the subsequent
stop_hook_active:truehandling works — but thesystemMessageno longer surfaces to the user.Environment
2.1.114 (Claude Code)(binary symlinked from~/.local/bin/claude)Stop+PreCompactvia pluginhooks.jsonbash ${CLAUDE_PLUGIN_ROOT}/hooks/mempal-stop-hook.sh→ invokespython -m mempalace hook run --hook stop --harness claude-codeWhat I observe
The hook runs. The hook saves data. The hook prints JSON to fd 1 (stdout), separate from any log noise on fd 2 (stderr). Verified by running the exact script Claude Code dispatches:
But in the terminal UI, nothing shows — not the
Stop says:line, not the✦, not even a collapsed● Ran N stop hookssummary. Silent success on the UI side even though the producer side is clean.Expected behavior
Per prior documentation and my own prior working session (2026-04-10 through 2026-04-17, pre-upgrade), a Stop hook outputting
{"systemMessage": "✦ ..."}renders as a visible single-line<Line>element viaAttachmentMessage.tsxhandlinghook_system_messageattachments. Described as: "silent success is invisible by design — but the systemMessage Line is the one visible signal."What I tried
/tmp/stop-hook-probeshows Claude Code invoking the script).continue: true, suppressOutput: falsealongsidesystemMessage— no change in rendering.So the regression appears to be on the UI side: either
AttachmentMessage.tsxno longer emits thehook_system_message<Line>for plugin-dispatched Stop hooks, or the attachment is being suppressed somewhere between hook-output capture and render.Repro (minimal)
Any plugin with a Stop hook that outputs
{"systemMessage": "test"}to stdout. Expected: a visibleStop says: testline. Actual: nothing visible, even though save-side side effects succeed.Impact
Any plugin using silent-save patterns (non-blocking, direct data writes with user-visible confirmation via
systemMessage) loses its only visibility signal. The pattern is deliberately non-blocking by design;systemMessagewas the UX bridge. Without rendering, users can't tell anything happened.Additional context
stop_hook_active:truewas silently eating auto-saves in block-mode-guarded silent hooks): filing on the MemPalace side as upstream-fix/silent-save-visibility.Happy to provide additional traces if helpful.