Desktop app model picker does not dispatch PreModelSwitch/PostModelSwitch hooks (CLI /model does)

Status Open
Reported on v2.1.266
Maintainer reply None cached
Activity 0 comments · opened Sep 13, 2026

Summary

Changing the session model from the model picker in the Claude desktop app's Code tab does not dispatch the PreModelSwitch or PostModelSwitch hooks. Using the /model command in a CLI TUI session dispatches both, with the same hook configuration, on the same machine.

The desktop binary is not missing the feature — it contains the dispatch symbols and the user-facing Running PreModelSwitch hooks… string. The picker appears to change the session model without entering the code path that dispatches the hooks.

Environment

| | |
|---|---|
| OS | macOS, Darwin 25.4.0, arm64 |
| Desktop app | Claude.app 1.52386.3 |
| Desktop-bundled Claude Code | 2.1.266 (~/Library/Application Support/Claude/claude-code/2.1.266/claude.app/Contents/MacOS/claude) |
| CLI Claude Code | 2.1.258 (@anthropic-ai/claude-code/bin/claude.exe) |
| Hook scope | user settings (~/.claude/settings.json), empty matcher on both events |

Both builds are above the documented 2.1.251 minimum for these events.

Reproduction

  1. Wire a PreModelSwitch and a PostModelSwitch hook in ~/.claude/settings.json with an empty matcher. Any command that appends a line to a file is enough.
  2. CLI: start claude --model sonnet in a terminal, then run /model haiku.
  3. Desktop: in the Claude desktop app's Code tab, change the model using the app's model picker.
  4. Compare the hook log.

Expected

Both surfaces dispatch PreModelSwitch before the switch is applied and PostModelSwitch after, per the hooks documentation, which describes PreModelSwitch as firing "before Claude Code applies a model switch that you or a client requested".

Actual

CLI — works. The TUI displays:

Running PreModelSwitch hooks… (Esc to cancel)
⎿ Set model to Haiku 4.5 and saved as your default for new sessions

and both hooks write their rows, in order, in the same second:

PreModelSwitch    from=claude-sonnet-5  to=claude-haiku-4-5-20251001
PostModelSwitch   from=claude-sonnet-5  to=claude-haiku-4-5-20251001

Desktop picker — no dispatch. Two separate model changes through the app's picker, by hand, produced zero rows for either event. Across the whole log neither event name has ever appeared from a desktop session.

Why this is not a configuration or registration problem

  • Other hooks fire normally in the very same desktop sessions, from the same settings file, throughout both test windows: SessionStart, UserPromptSubmit, PreToolUse, SubagentStart and Stop all produced rows. During the second picker switch alone, 66 rows were written across nine concurrent sessions. Only the two model-switch events are missing.
  • Newly added hook events do attach to already-running sessions. SubagentStart was added to settings in the same edit as the two model-switch events and fired one minute later for a session that had started before the edit (it has no SessionStart row at all). So "the session predates the hook" does not explain it.
  • The desktop binary contains the feature. strings word-match counts, CLI 2.1.258 vs desktop 2.1.266:

| symbol | CLI | desktop |
|---|---|---|
| PreModelSwitch | 41 | 40 |
| PostModelSwitch | 19 | 17 |
| to_model / from_model | 17 / 14 | 17 / 14 |
| Running PreModelSwitch hooks | 3 | 3 |

The last row is the significant one: the desktop build carries the same UI string the CLI prints while it awaits the hook.

  • Delivery does not depend on authentication — the CLI session used for the positive control showed Not logged in and the hooks fired anyway.

Impact

PreModelSwitch is documented as able to block a switch, which makes it the only supported enforcement point for policy on the session's own model (cost controls, restricting which models a session may move to, audit of model changes). Those policies hold in CLI sessions and are silently bypassed by the desktop picker — silently, because the hook simply never runs, so there is no error and nothing in any log to indicate the control did not apply.

PostModelSwitch is documented as also covering switches Claude Code makes itself, such as restoring the model on resume, so the same gap affects passive auditing of the served model, not just enforcement.

Notes on evidence

Samples are n=2 on the desktop picker (both negative) and n=1 on the CLI (positive). The measured quantity is the absence of the hook rows; that the picker switches themselves occurred is reported by the person who performed them, since a model change with no intervening turn leaves no other trace.

View original on GitHub ↗