Cursor-agent: imported Claude Code plugin hooks get a Cursor-shaped payload, so PreToolUse gates fail open

Describe the Bug

Version: cursor-agent CLI 2026.09.26-dd393fe (reproduced on macOS arm64 and Linux x64; also present in 2026.09.08 / 09.10 / 09.18)
Feature: importing Claude Code plugins (~/.claude/plugins/installed_plugins.json) and converting their hooks/hooks.json

When cursor-agent runs a hook imported from a Claude Code plugin, what the hook receives and how its response is read do not match the Claude Code hook contract. Plugins that enforce policy in PreToolUse hooks (for example “block git commit until review passes”) are silently bypassed under Cursor, with no error shown.

1. tool_name is Cursor’s name, not the Claude name the hook matched on

The converter maps the Claude matcher Bash to Cursor’s Shell tool, so the hook fires. But the stdin payload says "tool_name": "Shell". A Claude hook that checks tool_name == "Bash" concludes the call is not its concern and allows it.

Expected: for claude-plugin hooks, report the Claude tool name that the matcher was translated from (Shell → Bash).

2. cwd is an empty string for Shell calls

The payload has "cwd": "" and "tool_input": {"cwd": ""}. The real directory appears only in workspace_roots. A hook that resolves “which repository is this command in” from cwd falls back to its own process cwd, which is inside the plugin cache, and evaluates the wrong repository.

Expected: cwd set to the directory the command runs in, as in Claude Code.

3. Exec-form hooks lose args

Claude Code supports {"type":"command","command":"/path/launcher","args":[...]}. The converter copies only command, so the launcher starts with no arguments.

Expected: carry args through, either as argv or safely quoted into the command string, with ${CLAUDE_PLUGIN_ROOT} substituted before quoting.

4. Converted hooks are always failClosed: false

A hook that times out or cannot spawn is dropped, and the tool proceeds. For a policy hook that is a fail-open.

Expected: let the plugin declare fail-closed.

5. Legacy {"decision":"block","reason":...} is ignored for PreToolUse

The top-level legacy decision object is not translated into a deny. Only hookSpecificOutput.permissionDecision is honoured, and translating that one depends on enableClaudeNestedHookSpecificOutputCompatibility. Claude Code itself still honours the legacy shape.

Minimal repro (1 + 2)

  1. In a scratch directory, create .cursor/hooks.json with a preToolUse hook that copies stdin to a file.
  2. Run: cursor-agent -p --trust --force "Run the shell command: echo hi"
  3. Inspect the captured payload: "tool_name":"Shell", "cwd":"", "tool_input":{"command":"echo hi","cwd":"","timeout":30000}.

For the end-to-end effect, install any Claude Code plugin whose PreToolUse Bash hook denies git commit, then ask cursor-agent to commit in a git repo. The commit succeeds.

Steps to Reproduce

  1. Create a minimal Claude Code plugin:
    P=$HOME/cc-repro-plugin
    mkdir -p “$P/.claude-plugin” “$P/hooks”
    echo ‘{“name”:“cc-repro”,“version”:“1.0.0”}’ > “$P/.claude-plugin/plugin.json”
    cat > “$P/hooks/log.sh” <<‘EOF’
    #!/bin/bash
    { echo “argv: $*”; cat; echo; } >> /tmp/cc-hook.log
    echo ‘{“decision”:“block”,“reason”:“repro: blocked by plugin hook”}’
    EOF
    chmod +x “$P/hooks/log.sh”
    cat > “$P/hooks/hooks.json” <<‘EOF’
    {“hooks”:{“PreToolUse”:[{“matcher”:“Bash”,“hooks”:[
    {“type”:“command”,“command”:“${CLAUDE_PLUGIN_ROOT}/hooks/log.sh”,“args”:[“hello”],“timeout”:10}
    ]}]}}
    EOF

  2. Register and enable it the way Claude Code does:

    • in ~/.claude/plugins/installed_plugins.json, add under "plugins":
      "cc-repro@local": [{"scope":"user","installPath":"<$HOME>/cc-repro-plugin","version":"1.0.0"}]
    • in ~/.claude/settings.json, add: "enabledPlugins": {"cc-repro@local": true}
  3. In any git repo, run:
    cursor-agent -p --trust --force "Run the shell command: echo hi"

  4. Look at /tmp/cc-hook.log and at whether echo hi ran.

Actual (cursor-agent 2026.09.26-dd393fe, unmodified):

  • argv: is empty: args was dropped (#3)
  • the payload has "tool_name":"Shell" (#1)
  • the payload has "cwd":"" and "tool_input":{"command":"echo hi","cwd":"","timeout":30000} (#2)
  • echo hi runs even though the hook returned {"decision":"block",...} (#5)

Expected (same plugin under Claude Code):

  • argv: hello
  • "tool_name":"Bash"
  • cwd is the repo directory
  • the tool call is blocked with the hook’s reason

Operating System

MacOS
Linux

Version Information

cursor-agent CLI 2026.09.26-dd393fe

  • macOS 27.0 (26A428), arm64
  • Ubuntu 24.04.5 LTS, x86_64
    Also present in 2026.09.08, 2026.09.10 and 2026.09.18.

Does this stop you from using Cursor

Yes - Cursor is unusable

Hey @Chris_Yau, What you’re describing isn’t intended behavior for imported Claude Code plugin hooks, and we’ve let the team know.

To keep a PreToolUse gate holding in cursor-agent today:

  1. Deny with the newer shape instead of the legacy {"decision":"block"}: {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"..."}} (or exit code 2) — the CLI honors both.
  2. Treat tool_name "Shell" the same as "Bash".
  3. When cwd is empty, resolve from CURSOR_PROJECT_DIR / CLAUDE_PROJECT_DIR or workspace_roots[0].
  4. Put the arguments in the command string instead of args.
  5. For fail-closed, register the script in ~/.cursor/hooks.json with "matcher":"Shell" and "failClosed":true.

Full hooks reference: Hooks | Cursor Docs. If any of these still let it through, drop the captured payload + hook output here and I’ll dig in.