Skip to content

bug: Claude Code plugin UserPromptSubmit hook never persists prompts (POST /prompts is never called) #509

@ceeesar13

Description

@ceeesar13

📋 Pre-flight Checks

  • I have searched existing issues and this is not a duplicate
  • I understand this issue needs status:approved before a PR can be opened

📝 Bug Description

The Claude Code plugin ships a UserPromptSubmit hook (plugin/claude-code/scripts/user-prompt-submit.sh, and the .ps1 variant) whose manifest advertises "passive capture", but in practice the hook never persists the user's prompt.

On the first message of a session it injects the ToolSearch instruction; on subsequent messages it only emits save-nudges (curl GET ...). It reads the hook payload from stdin but never extracts .prompt and never calls POST /prompts.

Net effect: the prompts table stays near-empty while observations keep accumulating, so mem_save's best-effort prompt capture has nothing in SessionActivity to attach. Memories lose their originating-prompt context and recall quality degrades.

Evidence (two independent environments):

  • Local machine: 470 observations across 16 sessions, but only 5 prompts — and those 5 came from rare manual mem_save_prompt MCP calls, not the hook.
  • A separate machine syncing to a self-hosted Engram Cloud instance (VPS): same result — no prompts persisted beyond the few manual ones.

Platform-independent — confirmed on main (plugin 0.1.1), both user-prompt-submit.sh and user-prompt-submit.ps1.

🔄 Steps to Reproduce

  1. Install the Engram plugin in Claude Code and run engram serve.
  2. Have a normal multi-message session in any project (let the agent call mem_save a few times).
  3. Run engram stats (or GET /prompts/recent).
  4. Observe: Observations grows with each save, but Prompts stays at ~0 — it only ticks up on the rare occasions the agent calls mem_save_prompt itself.

✅ Expected Behavior

The UserPromptSubmit hook should persist each user prompt to Engram via POST /prompts with {session_id, content, project}. The session is already registered by session-start.sh under the same Claude session_id, and validateSessionProject passes when the project matches or is empty — so the call succeeds and mem_save can attach the originating prompt and dedupe via SessionActivity, as the protocol describes.

❌ Actual Behavior

The hook never calls POST /prompts; the prompt read from stdin is discarded. In user-prompt-submit.sh, INPUT=$(cat) is read but only .cwd and .session_id are extracted — never .prompt — and there is no write path to the server.

🔧 Suggested Fix (validated end-to-end against the local server)

Add a prompt-persist step in the hook (after the first-message branch):

PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty')
[ -n "$PROMPT" ] && curl -sf -X POST "${ENGRAM_URL}/prompts" --max-time 2 \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --arg s "$SESSION_ID" --arg p "$PROJECT" --arg c "$PROMPT" \
        '{session_id:$s, project:$p, content:$c}')" >/dev/null 2>&1

Plus the Invoke-RestMethod equivalent in user-prompt-submit.ps1.

I verified this exact POST /prompts call works end-to-end: piping a simulated Claude UserPromptSubmit payload through the hook incremented /prompts/recent and stored the prompt with the correct project attribution. As a local workaround I wired an additional UserPromptSubmit hook in settings.json that does this POST, and prompt capture now works — but the fix belongs in the plugin so every user benefits.

🖥️ Environment

  • OS: Windows 11 (also reproduced via Engram Cloud on a Linux VPS)
  • engram: 1.16.3
  • plugin: 0.1.0 (bug also confirmed on main / 0.1.1)
  • Agent: Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions