[BUG] Claude Code in the Claude Desktop app runs UserPromptSubmit hooks and enforces blocks, but renders no user-facing hook feedback (CLI renders all)

Open 💬 0 comments Opened Jun 9, 2026 by YehonaOnyx

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

When using Claude Code inside the Claude Desktop app (the coding mode embedded in the desktop app — the engine bundled at ~/Library/Application Support/Claude/claude-code/<ver>/), a UserPromptSubmit hook executes and its block takes effect, but the desktop app renders none of the documented user-facing hook feedback channels. To the user the turn just appears to hang. The exact same hook and settings.json, run under the standalone Claude Code CLI, render every channel correctly.

This is not a settings-resolution failure:

  • The hook does run in the desktop app — the hook logs every invocation and exactly what it emitted (see Evidence below); it fires once per submit.
  • The block does take effect — for the blocking channels the turn does not proceed.
  • Only the user-facing rendering is missing — the standalone CLI with the identical settings.json shows everything.
Channel matrix (same hook, same settings.json)

| Hook output | Claude Code CLI | Claude Code in Desktop app |
|---|---|---|
| exit 2 + stderr | ✅ blocks, stderr shown to user | ❌ blocks, nothing shown — appears to hang |
| {"decision":"block","reason":…} | ✅ blocks, reason shown | ❌ blocks, reason not shown — appears to hang |
| {"continue":false,"stopReason":…} | ✅ stops, stopReason shown | ❌ stops, stopReason not shown — appears to hang |
| {"systemMessage":…} (non-blocking) | ✅ warning shown to user | ❌ warning not shown |
| plain stdout (context injection) | ✅ injected | ✅ injected |

systemMessage is documented as a warning shown to the user; in the desktop app it is silently dropped. The block-reason channels (exit 2 stderr, decision:block reason, continue:false stopReason) are documented as user-facing for UserPromptSubmit, and in the desktop app they leave the user with an indistinguishable hang — the agent stops with no explanation.

Minimal Reproduction

settings.json:

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [ { "type": "command", "command": "/tmp/dt-repro/hook.sh" } ] }
    ]
  }
}

/tmp/dt-repro/hook.sh (chmod +x) — branches on a keyword in the prompt so all five channels are testable with one config, and logs every invocation with the exact output it emits:

#!/usr/bin/env bash
# UserPromptSubmit hook. ALWAYS logs that it was invoked and exactly what it
# emitted (channel + payload), then emits it. The log is written by the hook
# itself, so it is identical whether the CLI or the desktop app invoked it.
LOG=/tmp/dt-repro/fired.log
input=$(cat)
prompt=$(printf '%s' "$input" | python3 -c "import json,sys;print(json.load(sys.stdin).get('prompt',''))")
log() { printf '%s  prompt="%s"  ->  %s\n' "$(date '+%H:%M:%S')" "$prompt" "$1" >> "$LOG"; }

case "$prompt" in
  *EXIT2*)    out="MARK exit2+stderr"
              log "channel=exit2+stderr  emitted=$out"; echo "$out" >&2; exit 2 ;;
  *DECISION*) out='{"decision":"block","reason":"MARK decision:block reason"}'
              log "channel=stdout-json   emitted=$out"; echo "$out"; exit 0 ;;
  *STOP*)     out='{"continue":false,"stopReason":"MARK continue:false stopReason"}'
              log "channel=stdout-json   emitted=$out"; echo "$out"; exit 0 ;;
  *SYSMSG*)   out='{"systemMessage":"MARK systemMessage (docs: shown to the user)"}'
              log "channel=stdout-json   emitted=$out"; echo "$out"; exit 0 ;;
  *REPLACE*)  out="MARK plain-stdout replacement"
              log "channel=stdout-text   emitted=$out"; echo "$out"; exit 0 ;;
  *)          log "channel=none          emitted=(passthrough, no match)"; exit 0 ;;
esac

Steps:

  1. CLI baseline: claude --settings ./settings.json, then submit each of EXIT2 hi, DECISION hi, STOP hi, SYSMSG hi, REPLACE hi. Every MARK … string renders in the transcript.
  2. Desktop app: open the same project in Claude Code coding mode in the Claude Desktop app and submit the same five prompts. Only REPLACE hi surfaces anything; the four others produce no visible output — the blocking ones look like a silent hang.
  3. Confirm the hook ran in the desktop app: cat /tmp/dt-repro/fired.log (see Evidence).

Evidence — the hook is invoked and produces output, even in the desktop app

The hook records every invocation. After submitting the five prompts, /tmp/dt-repro/fired.log contains:

16:53:41  prompt="EXIT2 hi"     ->  channel=exit2+stderr  emitted=MARK exit2+stderr
16:53:41  prompt="DECISION hi"  ->  channel=stdout-json   emitted={"decision":"block","reason":"MARK decision:block reason"}
16:53:41  prompt="STOP hi"      ->  channel=stdout-json   emitted={"continue":false,"stopReason":"MARK continue:false stopReason"}
16:53:41  prompt="SYSMSG hi"    ->  channel=stdout-json   emitted={"systemMessage":"MARK systemMessage (docs: shown to the user)"}
16:53:41  prompt="REPLACE hi"   ->  channel=stdout-text   emitted=MARK plain-stdout replacement

This log is written by the hook itself, so the same lines appear whether the prompt was submitted in the CLI or the desktop app — confirming the hook is invoked in the desktop app and emits the output above. Yet in the desktop app none of it (except the plain-stdout REPLACE) reaches the user: the block is enforced, the reason/warning is discarded, and the UI just appears to hang.

What Should Happen?

Claude Code in the desktop app should surface the same user-facing hook feedback as the CLI — at minimum:

  • the block reason (exit 2 stderr / decision:block reason / continue:false stopReason), so a hook-blocked turn is not an unexplained hang, and
  • systemMessage warnings.

At an absolute minimum, when a hook blocks a turn, the desktop app should show something rather than appearing to hang.

Environment

  • OS: macOS (Darwin 24.6.0, arm64)
  • Claude Desktop app: 1.11187.4
  • Claude Code engine bundled with the Desktop app: 2.1.165 (~/Library/Application Support/Claude/claude-code/2.1.165/)
  • Standalone Claude Code CLI (baseline that renders correctly): 2.1.169

View original on GitHub ↗