Skip to content

UserPromptSubmit hook emits a ToolSearch selector that can never match in a plugin-only install #670

Description

@BGamboa13

Summary

plugin/claude-code/scripts/user-prompt-submit.{ps1,sh} emits a systemMessage instructing the agent, as a "CRITICAL FIRST ACTION", to run a ToolSearch with a hardcoded select: list of mcp__engram__* tool names.

When Engram is installed as a Claude Code plugin, Claude Code namespaces the plugin's MCP tools as mcp__plugin_<plugin>_<server>__* — i.e. mcp__plugin_engram_engram__mem_save. The hardcoded mcp__engram__* names therefore match nothing, and the mandated first action is a guaranteed no-op on every single prompt.

The names are correct for the other installation mode (Engram registered directly as an MCP server, where the server is literally named engram). The script is shared by both paths but written for only one.

Reproduction

  1. Install Engram via the plugin marketplace only — no direct mcpServers.engram entry for the project.
  2. Start a session and send any prompt.
  3. The hook injects:
CRITICAL FIRST ACTION — Execute this ToolSearch NOW before responding to the user:
select:mcp__engram__mem_save,mcp__engram__mem_search,mcp__engram__mem_context,...
  1. The tools actually present in the session are mcp__plugin_engram_engram__mem_save, mcp__plugin_engram_engram__mem_search, … so the selector resolves to zero tools.

Location

  • plugin/claude-code/scripts/user-prompt-submit.ps1Write-ToolSearchMessage, the $message string (line ~22)
  • plugin/claude-code/scripts/user-prompt-submit.sh — same hardcoded list
  • plugin/codex/scripts/user-prompt-submit.sh — same hardcoded list

Verified present on the default branch as of today, so v1.20.0 does not fix this — I checked the file contents at HEAD via the GitHub contents API, not just my locally cached 0.1.1 copy.

Why this has probably gone unnoticed

The bug is masked whenever Engram is also registered as a plain MCP server for the project — for example when installed through gentle-ai or a similar meta-installer that runs engram setup <agent> and writes an mcpServers.engram entry. In that configuration mcp__engram__* genuinely exists, the selector matches, and the hook works.

So the failure is specific to the plugin-only configuration, which is the marketplace's own primary distribution path.

Impact

Not a correctness bug — the core memory tools are already available without any ToolSearch, so memory still works. The cost is:

  1. Wasted work on every turn. The message is phrased as a mandatory first action, so a compliant agent spends a tool call resolving an empty selector before it can respond. With one of my own installs showing usageCount: 6491, that is a lot of turns.
  2. It teaches agents to disregard the hook. An instruction that is emphatic ("CRITICAL FIRST ACTION") and verifiably inert is worse than no instruction: the next directive from the same channel carries less weight.

Suggested fix

The robust fix is not to correct the prefix — either literal is wrong in one of the two supported installation modes. The hook should not hardcode the tool namespace at all, because the script cannot know which mode it was installed under.

Options, roughly in order of preference:

  1. Emit both namespaces in the select: list (mcp__engram__mem_save,mcp__plugin_engram_engram__mem_save,…). Unmatched names are harmless, and it is a one-line change that works in both modes.
  2. Detect the mode — the plugin hook runs with CLAUDE_PLUGIN_ROOT set, which distinguishes a plugin invocation from a direct MCP-server install, and pick the prefix accordingly.
  3. Drop the select: list entirely and keep only the second half of the message ("call mem_context to check for prior session history"). The core tools are loaded without ToolSearch anyway, so the selector may be buying nothing even when it does match — worth measuring before keeping it.

A note on the underlying shape

The hook maintains a hardcoded parallel list of the MCP server's own tool surface. That list has to agree with the server's actual tool names, and nothing enforces the agreement — so it silently drifts the moment the namespace changes, which is exactly what happened here. If the list stays, deriving it from the server's tool registry (or generating it at build time from the same source the server uses to register them) would make this class of drift impossible rather than merely fixed once.

Environment

  • Engram plugin 0.1.1 from the engram marketplace; binary 1.19.0 (1.20.0 available — checked, not fixed there either)
  • Claude Code on Windows (user-prompt-submit.ps1 path); the .sh variants carry the identical list
  • Reproduced in a project with enabledPlugins: {"engram@engram": true} and no mcpServers.engram entry

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions