Desktop app model picker does not dispatch PreModelSwitch/PostModelSwitch hooks (CLI /model does)
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
- Wire a
PreModelSwitchand aPostModelSwitchhook in~/.claude/settings.jsonwith an empty matcher. Any command that appends a line to a file is enough. - CLI: start
claude --model sonnetin a terminal, then run/model haiku. - Desktop: in the Claude desktop app's Code tab, change the model using the app's model picker.
- 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,SubagentStartandStopall 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.
SubagentStartwas 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 noSessionStartrow at all). So "the session predates the hook" does not explain it. - The desktop binary contains the feature.
stringsword-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 inand 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.