Rewind picker is unusable in sessions driven by plugin channel notifications (`isMeta:true` filter)

Resolved 💬 2 comments Opened Apr 7, 2026 by RocStone Closed May 20, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

TL;DR

When all user input in a Claude Code session arrives via a plugin's notifications/claude/channel MCP notification (e.g. the official discord plugin, or any future Slack / Telegram / email bridge built on the same channel API), Claude Code core stamps every such message with isMeta: true. The Esc+Esc / /rewind picker filters out all isMeta messages, so the picker shows "Nothing to rewind" even when the session contains dozens of real human messages from the user. The rewind feature is effectively dead for any "talk to Claude through a chat bridge" workflow.

The root cause is in Claude Code core, not in the plugin. The fix is small.

Environment

  • Claude Code version: 2.1.92 (observed in session JSONL version field)
  • Plugin: discord@claude-plugins-official v0.0.4 (gitCommitSha b664e152af5742dd11b6a6e5d7a65848a8c5a261)
  • OS: Windows 11
  • Reproducible on the latest plugin version (no newer version available at time of report)

Steps to Reproduce

  1. Install the official Discord plugin: /plugin install discord@claude-plugins-official
  2. Pair a Discord channel via /discord:access
  3. Start a fresh Claude Code session: claude
  4. Do not type anything from the TUI keyboard. Send all prompts to Claude exclusively via the Discord channel
  5. Have a multi-turn conversation (anything from 2 to N user messages, all from Discord)
  6. Press Esc+Esc (or run /rewind) to open the rewind picker

Expected: the picker lists every user message in the session as a rewindable anchor, just as it does for messages typed directly into the TUI.

Actual: the picker shows "Nothing to rewind". None of the Discord-originated user messages appear as anchors.

Verification that this is specifically about channel-injected input

In the same session, type any message directly from the TUI keyboard (a single character is enough). Then press Esc+Esc again. The picker now shows that one TUI-typed message as an anchor — but still none of the Discord-originated messages. This isolates the bug to channel-injected input.

Root Cause

Investigation path

  1. Hypothesis 1 (wrong): Discord messages are stored as something other than role:user. Inspected the session JSONL and found 18 records with type:"user" — so they are stored as user messages.
  2. Hypothesis 2 (correct): Of those 18, only 3 are real user input from Discord; the other 15 are tool_result records (which the Anthropic message protocol also wraps with role:user). All 3 real Discord-originated messages have isMeta: true and origin.kind: "channel". None of the tool_result records have isMeta.
  3. Verified the plugin is not at fault: grepped 893 lines of server.ts in claude-plugins-official/discord/0.0.4/. Zero matches for isMeta. The plugin only emits the message via:

``typescript
mcp.notification({
method: 'notifications/claude/channel',
params: {
content,
meta: { chat_id, message_id, user, user_id, ts, ... }
}
})
``

The plugin has no opportunity to set isMeta on the resulting Claude Code message — that field does not exist in the notification params schema. It is added by Claude Code core when it receives a notifications/claude/channel notification and converts it into a session message.

Evidence: session JSONL excerpts

A real Discord-originated user message in ~/.claude/projects/<project>/<session-id>.jsonl (with chat IDs and content redacted):

{
  "parentUuid": null,
  "isSidechain": false,
  "promptId": "<uuid>",
  "type": "user",
  "message": {
    "role": "user",
    "content": "<channel source=\"plugin:discord:discord\" chat_id=\"<redacted>\" ...>\n<the user's actual prompt>\n</channel>"
  },
  "isMeta": true,
  "uuid": "<uuid>",
  "timestamp": "2026-04-07T18:24:35.888Z",
  "permissionMode": "bypassPermissions",
  "origin": { "kind": "channel", "server": "plugin:discord:discord" },
  "userType": "external",
  "entrypoint": "cli",
  "sessionId": "<uuid>",
  "version": "2.1.92",
  "gitBranch": "main",
  "cwd": "<path>"
}

Key fields:

  • type: "user" — stored as a user message
  • message.role: "user" — user role at the protocol level
  • isMeta: truethis is the field that hides it from the rewind picker
  • origin.kind: "channel" — marks it as channel-injected (not direct TUI input)
  • userType: "external" — distinguishes external (Discord user) from local TUI

A counts-by-type dump for a real session that talked exclusively via Discord (then a few subagent / tool calls):

   29  assistant
   18  user            ← 3 are real Discord input (all isMeta:true), 15 are tool_result wrappers
    6  queue-operation
    3  attachment
    3  system
    1  permission-mode
    1  custom-title
    1  agent-name

After pressing Esc+Esc, the rewind picker correctly identifies 0 of the 3 real user messages as anchors, because they all carry isMeta: true.

Conclusion

isMeta: true is set by Claude Code core when receiving notifications/claude/channel notifications. The intent is presumably to distinguish "real human keystrokes in the TUI" from "system-injected pseudo-user content" (hooks, channel notifications, summarization injections, etc.). But the rewind picker treats isMeta as a blanket "skip me" signal, which lumps legitimate channel-routed human input together with system-internal pseudo messages and hides them all.

The semantic distinction the codebase wants to make is at least three-way:

  1. Real TUI user keystroke
  2. Channel-routed real human user message (Discord, future Slack, etc.)
  3. System-internal pseudo-user content (hook output, synthetic injections)

But isMeta only encodes a two-way distinction (1 vs 2+3), and the rewind picker treats categories 2 and 3 the same way. This is the bug.

Proposed Fixes

Three options, in order of how invasive they are:

Fix A — minimum-diff: stop filtering channel-origin messages from the rewind picker

In whatever code path filters rewind anchors, change:

// pseudo
const anchors = userMessages.filter(m => !m.isMeta)

to:

// pseudo
const anchors = userMessages.filter(m =>
  !m.isMeta || m.origin?.kind === 'channel'
)

This treats origin.kind === 'channel' as a "real human input via a side channel" override that bypasses the meta filter. No protocol or storage change needed. Backwards compatible.

Fix B — stop tagging channel notifications as meta in the first place

When Claude Code core ingests a notifications/claude/channel notification and synthesizes the user message, do not set isMeta: true. The origin.kind: "channel" field is already enough to distinguish it from a TUI keystroke, and any code path that genuinely needs "exclude channel input" can check origin.kind instead of isMeta.

This is cleaner long-term but may have more downstream callers that currently rely on isMeta to gate channel input. Each one would need to be audited.

Fix C — three-way distinction with a new field

Introduce a new field, e.g. userMessageSource: "tui" | "channel" | "synthetic". Migrate isMeta consumers to check the new field. The rewind picker filters out only synthetic, not channel.

This is the most principled fix but the largest change.

Recommended: Fix A. It is one line, fully backwards compatible, and immediately restores rewind for every plugin that uses notifications/claude/channel.

Impact / Scope

This affects every plugin that uses notifications/claude/channel to inject user input into a Claude Code session, not just the Discord plugin. That includes:

  • The official discord plugin (current)
  • Any Telegram / Slack / Matrix / email bridge plugin built on the same channel API (existing or future)
  • Any custom internal plugin a team builds on top of notifications/claude/channel

For users whose primary interaction modality with Claude Code is a chat bridge rather than the local TUI (a growing pattern), /rewind is currently completely unusable, which removes one of the most important context-management tools in long-running sessions.

Current Workarounds

For users hitting this today, until it is fixed:

  1. Type any single message into the TUI keyboard during the session. This creates one non-meta user message that the rewind picker can anchor on. From there Esc+Esc works as expected — though only for messages after the TUI keystroke, not before it, because the picker still cannot see the channel-injected messages.
  2. Use /compact <instructions> as an approximate substitute. /compact only keep details related to task X, drop the task Y discussion will compress the channel-injected content even though /rewind cannot reach it. The original messages remain in the session JSONL and can theoretically be reached by a future /rewind if Fix A or B lands.
  3. Discipline: open a separate Claude Code session per task instead of multiplexing through one channel. claude --name <task> makes this manageable, but it is a workaround for an architectural defect, not a real fix.

Related Issues

These are related but distinct — none of them describe the channel notification + isMeta interaction:

  • anthropics/claude-code#31976 — general rewind UX, including soft-lock on Esc when history is empty (closed wontfix)
  • anthropics/claude-code#44772 — rewind picker shows no pre-compact messages for sessions > 5 MB (different root cause: compaction tree truncation)
  • anthropics/claude-code#44596 — long-running Bash tool conversation silently rewound via isMeta synthetic message (related: another isMeta side effect, on a different code path)
  • anthropics/claude-code#37933 — Telegram plugin inbound messages not surfaced (notification delivery problem, fixed)
  • anthropics/claude-code#36477 — --channels mode stops processing notifications after first response (open, different code path)
  • anthropics/claude-code#37498 — Telegram inbound notifications not appearing in conversation (closed)

This issue is the natural complement of #44596: that one is about an isMeta synthetic message rewinding too much; this one is about isMeta causing rewind to do nothing at all. Both point at the same underlying problem — isMeta is overloaded and the rewind subsystem treats all isMeta messages identically when they actually have very different semantics.

Suggested Next Steps for Maintainers

  1. Apply Fix A (one-line change in the rewind picker filter) as a fast unblock.
  2. Audit other consumers of isMeta to see whether any other user-visible features (history scrolling, /compact boundaries, summarization) are also incorrectly hiding channel-routed input.
  3. Consider Fix C as a follow-up to give the codebase a three-way distinction it can rely on long-term.

I am happy to test a candidate fix locally against my Discord-driven session if a build is provided.

What Should Happen?

see above

Error Messages/Logs

Steps to Reproduce

see above

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.92

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

_No response_

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗