Where does the bug appear (feature/product)?
Cursor IDE
Describe the Bug
Documented behaviour (docs/agent/hooks): “All matching hooks from every source run; when responses conflict, higher-priority sources take precedence during merge.” Priority: Enterprise → Team → Project → User.
Actual behaviour: For updated_input the precedence is inverted — the lowest-priority source’s mutation is applied and higher-priority mutations are discarded, silently.
Both hooks are invoked and both receive the original tool_input, so the first half of the documented rule holds — only the conflict resolution is inverted.
Steps to Reproduce
- Register a preToolUse hook in /Library/Application Support/Cursor/hooks.json (enterprise) that returns {“updated_input”: {“command”: "echo MARK_ENT >/dev/null; " + original}}
- Register a preToolUse hook in ~/.cursor/hooks.json (user) returning the same with MARK_USR
- Register a postToolUse hook to observe the executed command
- Ask the agent to run echo priority-test
Expected Behavior
Expected: MARK_ENT applied (enterprise is highest priority)
Actual: MARK_USR applied; MARK_ENT discarded with no error or diagnostic
Operating System
MacOS
Version Information
Environment: Cursor 3.17.8, macOS 26.3 (arm64), agent build 2026.08.11-e8db854
Additional Information
- Imported third-party (Claude Code) hooks are merged last, so they override every native scope including enterprise. Within ~/.claude/settings.json the array order decides, later winning. The docs place third-party hooks nowhere in the priority order.
- Security impact — privilege inversion. .claude/settings.local.json is project-local and typically user-writable and gitignored. A user can neutralise an enterprise-managed hook’s input mutation with an unreviewed file, and the enterprise hook receives no signal that its mutation was discarded.
Permission decisions behave correctly — a deny from any source blocks the call regardless of scope, so this is specific to input mutation. - args are dropped when importing Claude Code hooks. A hook registered as {“command”: “/path/bin”, “args”: [“selector”]} is invoked without the argv, so any hook using arguments to select behaviour silently misbehaves. Cursor sends its own camelCase payload to imported hooks (not translated to Claude’s schema), while accepting either response shape.
Does this stop you from using Cursor
No - Cursor works, but with this issue