Skip to content

Claude Code Stop hook silently reindexes Milvus Lite on every turn #629

Description

@M-Marbouh

Summary

At current main (0078d04578480f51f0eafe25ffbb8f64cbfcb1b7), the Claude Code Stop hook is asynchronous and runs memsearch index after every captured turn, including when the configured backend is Milvus Lite.

The corresponding Codex hook only indexes from Stop for HTTP/TCP Milvus Server backends. The Claude hook has no backend guard, and its run_memsearch helper redirects stderr and converts every non-zero exit to success with || true. A failed per-turn index is therefore invisible to the user.

Current code

  • plugins/claude-code/hooks/hooks.json:26-34: Stop runs with "async": true.
  • plugins/claude-code/hooks/stop.sh:174-178: kills a prior index process, then unconditionally calls run_memsearch index "$MEMORY_DIR".
  • plugins/claude-code/hooks/common.sh:126-132: suppresses stderr and ignores the command's exit status.
  • For comparison, plugins/codex/hooks/stop.sh:197-202 guards the same operation to http* / tcp* URIs.

Observed failure

On Linux with memsearch 0.4.14, ONNX embeddings, and the default local Lite database:

  • Markdown memory files continued to be written through July 16, while the affected collection's last segment write was a zero-byte data_003919_004001.parquet on July 3, with no later segments.
  • Against a byte-identical throwaway copy of the corrupt collection, an explicitly scoped memsearch search ... --collection <collection> exited 1 during DescribeCollection: Parquet file size is 0 bytes. It did not return stale rows from the readable segments.
  • A direct memsearch index <memory-dir> --force --collection <collection> also exited 1 on the same zero-byte segment. The failure was loud when invoked directly; the asynchronous Stop path hid it by suppressing stderr and converting the non-zero exit to success with || true.
  • Before repair, the manifest sequence was 4,001 but only 3,428 physical rows were readable. Rebuilding from the explicit memory path into a clean collection produced 4,082 chunks, and an exact sentence from the July 16 file returned its own source.

Collection-scope note: bare memsearch search <query> uses the unrelated default collection memsearch_chunks. An earlier revision of this report incorrectly treated results from that default collection as evidence about the affected project collection; those observations have been removed.

This report does not assert that Lite file-lock contention caused the zero-byte segment. The confirmed problem is that the async Claude Stop path invokes a Lite index every turn and suppresses any failure, leaving a corrupt or unupdated derived index without a Stop-hook signal even though later direct operations fail loudly.

Expected behavior

The Lite indexing path should be serialized/single-flight (or limited to the existing SessionStart fallback), and a failed index should leave a detectable signal rather than being silently treated as success.

Related reports

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