[BUG] MessageDisplay output is lost on resume: restored messages never pass through the hook (--resume, --continue, /resume)

Status Open
Reported on v2.1.294
Maintainer reply None cached
Activity 0 comments · opened Oct 8, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (related but different: #93531, #85773)
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.294)

What's Wrong?

MessageDisplay hook output only applies to messages as they stream. When a conversation is restored (claude --resume, claude --continue, the in-app /resume and /fork, and terminal apps that relaunch agents with --resume after a restart or hibernation), the restored assistant messages are drawn from the raw transcript and never passed to the hook. Whatever the hook did to the display is silently undone for the whole history.

This matters for any display hook:

  • a redaction hook's masking disappears on resume: the unredacted text is shown;
  • a renderer (in my case LaTeX → terminal graphics, cf. #44479 / #80702) shows raw source for the restored history.

What Should Happen?

Restored assistant messages should be displayed through MessageDisplay the same way live ones are (the hook docs: "While assistant message text is displayed … displayContent to replace the delta on screen").

Error Messages/Logs

None. The hook is simply not invoked: tracing every hook call during a resume shows 0 calls, against 1 per message live.

Steps to Reproduce

  1. Save this as settings.json. It is a display hook that upper-cases assistant text and needs jq:

``json
{"hooks":{"MessageDisplay":[{"hooks":[{"type":"command","command":"jq -c '{hookSpecificOutput:{hookEventName:\"MessageDisplay\",displayContent:(.delta|ascii_upcase)}}'"}]}]}}
``

  1. claude --settings settings.json, then send: Reply with exactly this sentence and nothing else: hello from the display hook

→ shown as ⏺ HELLO FROM THE DISPLAY HOOK (hook applied).

  1. /exit, then claude --settings settings.json --continue

→ the same message is shown as ⏺ hello from the display hook (hook not applied).

The same happens with --resume <id> and with /resume <id> typed inside a running session.

Claude Model

Not sure / Multiple models (not model-dependent; reproduced with Haiku)

Is this a regression?

No, this never worked (as far as I can tell)

Claude Code Version

2.1.294 (Claude Code), native install; also 2.1.291

Platform

Anthropic API (Claude Max subscription)

Operating System

macOS

Terminal/Shell

Other (cmux, Ghostty-based; fullscreen mode CLAUDE_CODE_NO_FLICKER=1)

Additional Information

Where it goes wrong. This is from reading the 2.1.294 bundle, so the names are as they appear there.

  • Live: the streaming controller runs the hook per batch and stores the result in appState.displayedMessageContent[apiMessageId]. The text-block renderer draws that entry in place of the raw text.
  • The map starts as {} and is never persisted.
  • Restore goes restoreOnMount / in-REPL resume → restoreReadFileState(messages) → turn.resetForConversation({kind: "resumed", messages}). That path never runs the hook, so the map stays empty for restored messages.
  • The SDK path already has a "completed message" runner. It calls the hook once with {index: 0, final: true, delta: <joined text blocks>}.

Suggested fix. When a conversation is restored (resetForConversation({kind: "resumed"})), or lazily on a render-time miss in displayedMessageContent for an assistant text block, run MessageDisplay once per restored assistant message the way the completed-message path does, and store the result in the same map.

  • Re-run rather than persist displayContent. A display hook can have side effects tied to the live terminal, such as kitty-graphics image uploads, and those are lost along with the pty.
  • Group by message id. Transcripts store one entry per content block, and the entries sharing a message.id are not always consecutive. So group by message.id and join the text blocks, as the live path's deltas do.

Feasibility. I tested this with a local patch to the binary on 2.1.291 and 2.1.294. It runs restored messages through a fresh instance of the live display controller at those two restore points. Afterwards, resumed transcripts (--resume, --continue, in-app /resume) render the same as live, including tables and inline images. A supported fix would let people drop that kind of patch.

View original on GitHub ↗