Skip to content

v5.1.4 β€” Cursor IDE Compatibility (shared user_preferences with Claude Code)

Choose a tag to compare

@ChanMeng666 ChanMeng666 released this 01 May 10:47
· 19 commits to master since this release

[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.json on 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 of runner/run.py. To pick up the 5.1.4 fix, run inside Claude Code: /plugin uninstall audio-hooks@chanmeng-audio-hooks then /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.py without setting CLAUDE_PLUGIN_DATA, so _resolve_config_file()'s legacy fallback chain in 5.1.3 returned the cached default_preferences.json (which ships audio_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 when CLAUDE_PLUGIN_DATA is set.

Added

  • hooks/hook_runner.py: _resolve_data_dir() β€” single source of truth for the audio-hooks state directory, used by get_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/ (if user_preferences.json exists) β†’ ~/.cursor/audio-hooks-data/ (if user_preferences.json exists) β†’ 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_VERSION for Cursor β€” set by Cursor's bridge per cursor.com/docs/hooks; CLAUDE_PLUGIN_DATA/CLAUDE_PLUGIN_ROOT for Claude Code). Env-var detection is more reliable than parsing stdin because it survives malformed payloads.
  • session_start hook auto-emits {"env": {"CLAUDE_PLUGIN_DATA": "<path>"}} to stdout when invoker is Cursor. Per cursor.com/docs/hooks, sessionStart env 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 of enabled_hooks.session_start because env propagation is a session-setup concern, not a notification concern. Claude Code path is unaffected (no env JSON is emitted because CURSOR_VERSION is unset).
  • bin/audio-hooks install --cursor β€” writes ~/.cursor/hooks.json from cursor-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.json from default_preferences.json, and writes ~/.cursor/audio-hooks-data/install_marker.json. Aborts with stable error code DUPLICATE_BRIDGE when Claude Code's audio-hooks plugin is already installed (because Cursor's auto-bridge would fire every event a second time); pass --force to install anyway.
  • bin/audio-hooks uninstall --cursor β€” removes only the _managed_by: "audio-hooks" entries, preserves any other user hooks, and deletes ~/.cursor/hooks.json entirely if it would be empty. Default keeps ~/.cursor/audio-hooks-data/ so re-install picks up the user's preferences; --purge removes that directory too.
  • audio-hooks status / diagnose / manifest output editor_targets β€” a per-editor JSON block reporting {state, via, ...} for claude-code and cursor. Cursor states: active / bridged-via-claude-code / native / double-registered / inactive. The double-registered state surfaces a DUPLICATE_BRIDGE warning explaining what to fix.
  • webhook_settings.include_user_email (default false) β€” opt-in flag for whether user_email from 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_invoker env-var matrix, _resolve_data_dir priority chain, session_start env-output for Cursor (and silence for non-Cursor), NDJSON invoker field on every event, webhook payload invoker + cursor sub-object, user_email redaction by default, opt-in surfacing.
  • cursor-hooks/hooks.json β€” canonical Cursor IDE hooks template. Maps 11 supported Cursor events to our hook names. stop and subagentStop set loop_limit: 0 to defensively prevent any accidental auto-resubmission via followup_message (Cursor's loop_limit defaults to 5 for native hooks). Documents Notification and PermissionRequest as 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. Notification and PermissionRequest have no Cursor equivalent β€” those audio cues will never fire from Cursor. Use Claude Code if you depend on them.
  • Glob / WebFetch / WebSearch matchers do not fire under Cursor β€” Cursor lacks these tool names, so pretooluse / posttooluse matchers 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 under D:\github_repository\... does not propagate to Cursor until the user runs /plugin uninstall + /plugin install inside Claude Code (or otherwise refreshes the cache). This is documented in the warning at the top of this entry.