v5.1.4 β Cursor IDE Compatibility (shared user_preferences with Claude Code)
[5.1.4] - 2026-05-01
β οΈ Cursor users β read this first. If you have audio-hooks installed in Claude Code, Cursor IDE 3.2.16+ already auto-bridges this project's hooks (per cursor.com/docs/reference/third-party-hooks) β Cursor's "Cursor Hooks Service" loads~/.claude/plugins/installed_plugins.jsonon startup and calls our hook scripts on its own session events. This means even if you uninstalled and reinstalled Claude Code's audio-hooks, Cursor was probably still calling the old cached version ofrunner/run.py. To pick up the 5.1.4 fix, run inside Claude Code:/plugin uninstall audio-hooks@chanmeng-audio-hooksthen/plugin install audio-hooks@chanmeng-audio-hooks. That refreshes~/.claude/plugins/cache/chanmeng-audio-hooks/audio-hooks/<ver>/to the new code Cursor will then bridge.
Cursor IDE compatibility. The runner now finds the user's real user_preferences.json whether it is invoked from Claude Code (which sets CLAUDE_PLUGIN_DATA) or from Cursor's auto-bridge (which does not). A new audio-hooks install --cursor subcommand registers natively for users who run Cursor without Claude Code. NDJSON events and webhook payloads now carry invoker (claude-code / cursor / unknown) plus a cursor sub-object surfacing Cursor-specific stdin fields (conversation_id, reason, final_status, duration_ms, ...). 13 new unit tests pin the contract.
Fixed
- Cursor IDE was playing the wrong audio theme even after the user switched themes in Claude Code (reported in chat by ai@gavigo.com). Cursor's auto-bridge invokes the cached plugin's
runner/run.pywithout settingCLAUDE_PLUGIN_DATA, so_resolve_config_file()'s legacy fallback chain in 5.1.3 returned the cacheddefault_preferences.json(which shipsaudio_theme: "default") instead of the user's actual~/.claude/plugins/data/audio-hooks-chanmeng-audio-hooks/user_preferences.json(which they had set to"custom"). The runner now has a centralized_resolve_data_dir()whose priority chain falls through to the well-known shared Claude Code data dir before the temp-dir fallback, so Cursor and Claude Code read the same preferences file. The change is backwards-compatible: behavior is unchanged whenCLAUDE_PLUGIN_DATAis set.
Added
hooks/hook_runner.py: _resolve_data_dir()β single source of truth for the audio-hooks state directory, used byget_log_dir,_resolve_queue_dir, and_resolve_config_file. Priority:CLAUDE_PLUGIN_DATAβCLAUDE_AUDIO_HOOKS_DATAβ~/.claude/plugins/data/audio-hooks-chanmeng-audio-hooks/(ifuser_preferences.jsonexists) β~/.cursor/audio-hooks-data/(ifuser_preferences.jsonexists) β legacy temp dir. Centralizing the chain meant the four call sites that previously had hand-coded fallbacks now agree on one definition.hooks/hook_runner.py: detect_invoker()β returns"claude-code"/"cursor"/"unknown"based on environment variables (CURSOR_VERSIONfor Cursor β set by Cursor's bridge per cursor.com/docs/hooks;CLAUDE_PLUGIN_DATA/CLAUDE_PLUGIN_ROOTfor Claude Code). Env-var detection is more reliable than parsing stdin because it survives malformed payloads.session_starthook auto-emits{"env": {"CLAUDE_PLUGIN_DATA": "<path>"}}to stdout when invoker is Cursor. Per cursor.com/docs/hooks,sessionStartenv outputs propagate to every subsequent hook in the same Cursor session β so after this one-time injection,stop,sessionEnd,preToolUse, etc. all see the correct preferences path without depending on the runtime fallback. The handler emits unconditionally regardless ofenabled_hooks.session_startbecause env propagation is a session-setup concern, not a notification concern. Claude Code path is unaffected (no env JSON is emitted becauseCURSOR_VERSIONis unset).bin/audio-hooks install --cursorβ writes~/.cursor/hooks.jsonfromcursor-hooks/hooks.json(new canonical template, schema v1, camelCase event names). Substitutes{{PYTHON}}and{{HOOK_RUNNER}}with absolute paths at install time, merges with any existing user hooks (each managed entry tagged"_managed_by": "audio-hooks"so we can find and remove only ours later), seeds~/.cursor/audio-hooks-data/user_preferences.jsonfromdefault_preferences.json, and writes~/.cursor/audio-hooks-data/install_marker.json. Aborts with stable error codeDUPLICATE_BRIDGEwhen Claude Code's audio-hooks plugin is already installed (because Cursor's auto-bridge would fire every event a second time); pass--forceto install anyway.bin/audio-hooks uninstall --cursorβ removes only the_managed_by: "audio-hooks"entries, preserves any other user hooks, and deletes~/.cursor/hooks.jsonentirely if it would be empty. Default keeps~/.cursor/audio-hooks-data/so re-install picks up the user's preferences;--purgeremoves that directory too.audio-hooks status/diagnose/manifestoutputeditor_targetsβ a per-editor JSON block reporting{state, via, ...}forclaude-codeandcursor. Cursor states:active/bridged-via-claude-code/native/double-registered/inactive. Thedouble-registeredstate surfaces aDUPLICATE_BRIDGEwarning explaining what to fix.webhook_settings.include_user_email(defaultfalse) β opt-in flag for whetheruser_emailfrom Cursor's stdin schema is included in webhook payloads. Off by default because the webhook URL may be a third-party service.tests/test_cursor_bridge.pyβ 13 unit tests covering:detect_invokerenv-var matrix,_resolve_data_dirpriority chain,session_startenv-output for Cursor (and silence for non-Cursor), NDJSONinvokerfield on every event, webhook payloadinvoker+cursorsub-object,user_emailredaction by default, opt-in surfacing.cursor-hooks/hooks.jsonβ canonical Cursor IDE hooks template. Maps 11 supported Cursor events to our hook names.stopandsubagentStopsetloop_limit: 0to defensively prevent any accidental auto-resubmission viafollowup_message(Cursor's loop_limit defaults to 5 for native hooks). DocumentsNotificationandPermissionRequestas deliberately absent β Cursor has no equivalent events (cursor.com/docs/reference/third-party-hooks).
Documented (known limitations)
- Cursor's bridge maps 8 of 10 Claude Code hooks.
NotificationandPermissionRequesthave no Cursor equivalent β those audio cues will never fire from Cursor. Use Claude Code if you depend on them. Glob/WebFetch/WebSearchmatchers do not fire under Cursor β Cursor lacks these tool names, sopretooluse/posttoolusematchers configured for them are silently skipped by Cursor.- The only Cursor-side opt-out is the global "Third-party skills" toggle in Cursor Settings β this disables auto-bridging for all Claude Code plugins, not just audio-hooks. There is no per-plugin Cursor opt-out today.
- Cursor caches the Claude Code plugin code under
~/.claude/plugins/cache/<id>/<ver>/. Editing this project's source underD:\github_repository\...does not propagate to Cursor until the user runs/plugin uninstall+/plugin installinside Claude Code (or otherwise refreshes the cache). This is documented in the warning at the top of this entry.