Claude Code hooks
The MessageDisplay hook in Claude Code
MessageDisplay fires as Claude’s reply is shown and can rewrite what appears on screen without changing what Claude or the transcript recorded.
Every other blocking-capable event in this list can change what actually happens next: a tool call, a turn ending, a config change taking effect. This one cannot. Whatever it returns only ever changes pixels on a screen, never the model's own memory of what it said.
- When it fires
- While assistant message text is being displayed.
- Can it block?
- No. The message is shown either way.
- Matcher
- No. It always fires.
What it receives
The event arrives as JSON on standard input. These are the fields worth reading.
| Field | Type | What it is |
|---|---|---|
message | string | The assistant text about to be displayed. |
prompt_id | string | The turn being displayed. |
session_id | string | Stable for the session. |
Exit codes
The exit code is the decision. Anything the script writes to standard error on a blocking exit is what Claude gets told.
| Exit | What happens |
|---|---|
0 | Success. |
2 | No blocking behavior. The original text is displayed. |
other | Non-blocking. The original text is displayed. |
What it can return
displayContent replaces what you see on screen and nothing else. The transcript keeps the original, and so does what Claude itself carries forward, which is the point: this is a display filter, not an edit.
{
"hookSpecificOutput": {
"hookEventName": "MessageDisplay",
"displayContent": "string"
}
}
Keeping secrets off a shared screen
Anything Claude echoes back can end up in a screen recording or a shared window. Masking token-shaped strings on the way to the screen costs nothing and does not alter what the session actually holds.
In settings.json
{
"hooks": {
"MessageDisplay": [
{
"hooks": [
{ "type": "command", "command": "node ~/.claude/hooks/mask.mjs", "timeout": 5 }
]
}
]
}
}
The script
const input = JSON.parse(require("fs").readFileSync(0, "utf8")) || {};
const masked = (input.message || "").replace(/\b(sk-[A-Za-z0-9]{8,})\b/g, "sk-REDACTED");
console.log(JSON.stringify({
hookSpecificOutput: { hookEventName: "MessageDisplay", displayContent: masked }
}));
process.exit(0);
Masking the display is not redaction. The real string is still in the transcript on disk.
The catch
The default timeout here is 10 seconds rather than the 600 most events get, and it runs while text is being drawn. Anything slow shows up as the reply stuttering.