Skip to content

Claude Code: SessionStart writes an empty ## Session HH:MM heading every session, polluting Recent Memory injection and the search index #598

Description

@camrontaylor

Summary

On the Claude Code platform, hooks/session-start.sh appends a ## Session HH:MM heading to the project shadow memory file (<gitroot>/.memsearch/memory/<date>.md) on every session start, unconditionally. The matching content is only written later by hooks/stop.sh, and only when the turn has real content. So any session that starts and ends without captured content leaves a permanent empty heading.

On a setup that opens many short sessions per day (git worktrees, headless claude -p, scheduled jobs, quick interactive checks), these empty headings accumulate fast and degrade two things:

  1. The SessionStart "# Recent Memory" cold-start injection. It reads the first ~40 heading/bullet lines of the two newest daily files. When a file opens with dozens of empty ## Session headings, the injection is almost all blank stubs and shows little or no real recent context. To the user this looks like the agent has "lost its memory" at session start.
  2. The search index. The empty headings are embedded as near-empty chunks. In Lite mode the per-turn memsearch index "$MEMORY_DIR" in stop.sh keeps re-embedding the shadow dir including these stubs, so they crowd out real content in recall results.

Environment

  • memsearch 0.4.7 (plugin), Claude Code, macOS, ONNX/local embedding, Milvus Lite.

Where it happens (0.4.7)

  • hooks/session-start.sh (~lines 113-115) writes the heading unconditionally:
    if [ ! -f "$MEMORY_FILE" ] || ! grep -qF "## Session $NOW" "$MEMORY_FILE"; then
      echo -e "\n## Session $NOW\n" >> "$MEMORY_FILE"
    fi
  • hooks/stop.sh writes the ### HH:MM content only when the transcript has >= 3 lines (~lines 48-53), plugins.<platform>.summarize.enabled is not false (~lines 57-61), and the parsed turn is non-empty (~lines 154-167). The content is conditional; the heading is not. Mismatch leaves empty headings.
  • hooks/session-start.sh (~lines 161-176) builds the injection from grep -E '^(#{2,4} |- )' "$f" | head -40 over the two newest files, so leading empty headings crowd out real content.

Real-world impact (one repo)

  • 2026-06-22 shadow file: 222 ## Session headings, 68 empty.
  • 2026-06-23: 134 headings, 36 empty.
  • Across 10 daily files: ~193 empty headings out of ~694 total (~28%).
  • Net effect: the cold-start injection was dominated by empty stubs in its first 40 lines, and semantic search surfaced shadow stubs over real notes.

No config workaround on Claude Code

The only related config is plugins.claude-code.summarize.enabled=false, which disables content capture entirely but leaves the empty headings and the injection in place, so it makes the symptom worse, not better. The autoRecall / allowPromptInjection toggles exist only for the OpenClaw platform, not Claude Code. There is no documented option to suppress the empty-heading write or the cold-start injection on Claude Code.

Proposed fixes (any one resolves it)

  1. Preferred - write the heading lazily. Have stop.sh write the ## Session HH:MM anchor on the first real summary for a session (keyed by session id), and drop the unconditional write in session-start.sh. Empty headings then cannot exist.
  2. Cheaper - self-heal on start. Before appending a new heading in session-start.sh, drop any immediately preceding empty ## Session heading (a heading with no non-blank body before the next heading or EOF).
  3. Most conservative - mitigate the injection and add a toggle. Skip empty sessions when building the "# Recent Memory" block (only include a ## Session heading that has body content), and/or add plugins.claude-code.autoRecall (default true) plus a session_heading toggle so Claude Code users can opt out, matching the OpenClaw toggles.

Fix 1 or 2 is the root fix; fix 3 mitigates the injection side.

I am happy to send a PR for option 1 or 2 if that is welcome.

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