TL;DR: the UserPromptSubmit hook (hooks/userprompt-claude.sh) prints JSON the current Claude Code validator rejects. Claude Code then shows a Hook JSON output validation failed banner on essentially every turn (the debounce no-op path emits the same rejected shape, and it fires on every turn inside the debounce window), and the memtrace-first nudge never reaches the model.
Found this while exercising the MCP surface. The MCP side is doing real work; this looks like a narrow rough edge on the Claude Code hook glue rather than anything in the graph engine.
Environment
- memtrace 0.6.0 (npm global)
- Claude Code 2.1.165
- macOS 26.4.1, Apple Silicon
- Install: documented setup (marketplace + plugin + MCP) plus the
memtrace install hooks
Symptom
Claude Code surfaces, on the user turn and again at session end:
UserPromptSubmit hook error
Hook JSON output validation failed (root): Invalid input
Where
hooks/userprompt-claude.sh (on npm-global macOS: /opt/homebrew/lib/node_modules/memtrace/hooks/userprompt-claude.sh). Two heredocs print the same shape:
- the nudge emit (around line 254)
- the debounce no-op emit (around line 173)
Both output:
{ "decision": "continue", "additionalContext": "..." }
Root cause
Based on the observed rejection, the validator appears to require every key in a UserPromptSubmit hook payload to be a known key, in its declared position, or it drops the whole object. Two keys here seem to break that.
decision appears to accept only "block" (or the field absent); "continue" looks like it is not in the enum. The script seems to conflate the top-level boolean continue field with a decision value, and the in-script comment (lines 31-32) cites an older doc shape. That likely explains the drift.
additionalContext appears to belong under hookSpecificOutput.additionalContext, paired with hookSpecificOutput.hookEventName: "UserPromptSubmit". At top level it reads as an unknown key.
Either way, the validator rejects at the root. That matches the (root): Invalid input you see.
Reproduction
printf '%s' '{"prompt":"please audit this code","session_id":"repro"}' \
| MEMTRACE_HOOK_DEBOUNCE_SECS=0 \
/opt/homebrew/lib/node_modules/memtrace/hooks/userprompt-claude.sh
Prints {"decision":"continue","additionalContext":"Memtrace is active..."}.
The debounce no-op path prints the same shape with an empty context. So the banner also fires on prompts that match no intent. Any turn inside the 120s debounce window of a live session triggers it, which is why it repeats all session long rather than only on code-discovery prompts.
Expected output
To inject context:
{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "..." } }
To inject nothing: exit 0 with empty stdout (or print {}). Never decision: "continue".
Impact
- A validation-error banner fires every turn. It reads like something is broken, even though the MCP side is fine.
- The nudge appears to be dropped (the validator rejects the object, so
additionalContext is not delivered), so the hook's actual job (steer toward the memtrace MCP tools before grep) never happens. That second effect is the one that probably stings: the feature is built, but the validation rejection prevents delivery.
Suggested fix
Rewrite both heredocs to the hookSpecificOutput shape. Two spots, small diff. Once that shape passes validation, the banner should clear and the nudge should reach the model on code-discovery turns. I might be missing a deliberate reason for the current shape, but if not, happy to send a PR if that saves you time.
Minor, while here
The nudge additionalContext string itself carries a U+2014 em-dash. Cosmetic, unrelated to the schema bug, flagging it in case memtrace wants em-dash-clean injected context.
Workaround for anyone hitting this now
MEMTRACE_HOOK_MODE=off makes the hook exit before any output, which stops the banner. You lose the nudge, but the memtrace-first skill already enforces the same discipline.
TL;DR: the
UserPromptSubmithook (hooks/userprompt-claude.sh) prints JSON the current Claude Code validator rejects. Claude Code then shows aHook JSON output validation failedbanner on essentially every turn (the debounce no-op path emits the same rejected shape, and it fires on every turn inside the debounce window), and the memtrace-first nudge never reaches the model.Found this while exercising the MCP surface. The MCP side is doing real work; this looks like a narrow rough edge on the Claude Code hook glue rather than anything in the graph engine.
Environment
memtrace installhooksSymptom
Claude Code surfaces, on the user turn and again at session end:
Where
hooks/userprompt-claude.sh(on npm-global macOS:/opt/homebrew/lib/node_modules/memtrace/hooks/userprompt-claude.sh). Two heredocs print the same shape:Both output:
{ "decision": "continue", "additionalContext": "..." }Root cause
Based on the observed rejection, the validator appears to require every key in a
UserPromptSubmithook payload to be a known key, in its declared position, or it drops the whole object. Two keys here seem to break that.decisionappears to accept only"block"(or the field absent);"continue"looks like it is not in the enum. The script seems to conflate the top-level booleancontinuefield with adecisionvalue, and the in-script comment (lines 31-32) cites an older doc shape. That likely explains the drift.additionalContextappears to belong underhookSpecificOutput.additionalContext, paired withhookSpecificOutput.hookEventName: "UserPromptSubmit". At top level it reads as an unknown key.Either way, the validator rejects at the root. That matches the
(root): Invalid inputyou see.Reproduction
Prints
{"decision":"continue","additionalContext":"Memtrace is active..."}.The debounce no-op path prints the same shape with an empty context. So the banner also fires on prompts that match no intent. Any turn inside the 120s debounce window of a live session triggers it, which is why it repeats all session long rather than only on code-discovery prompts.
Expected output
To inject context:
{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "..." } }To inject nothing:
exit 0with empty stdout (or print{}). Neverdecision: "continue".Impact
additionalContextis not delivered), so the hook's actual job (steer toward the memtrace MCP tools before grep) never happens. That second effect is the one that probably stings: the feature is built, but the validation rejection prevents delivery.Suggested fix
Rewrite both heredocs to the
hookSpecificOutputshape. Two spots, small diff. Once that shape passes validation, the banner should clear and the nudge should reach the model on code-discovery turns. I might be missing a deliberate reason for the current shape, but if not, happy to send a PR if that saves you time.Minor, while here
The nudge
additionalContextstring itself carries a U+2014 em-dash. Cosmetic, unrelated to the schema bug, flagging it in case memtrace wants em-dash-clean injected context.Workaround for anyone hitting this now
MEMTRACE_HOOK_MODE=offmakes the hook exit before any output, which stops the banner. You lose the nudge, but thememtrace-firstskill already enforces the same discipline.