claude-dev-env 2.21.1 → 2.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -71,6 +71,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
71
71
  | `bot_mention_comment_blocker.py` | PreToolUse (Write/Edit) | PR review comments that @-mention a bot |
72
72
  | `claude_md_orphan_file_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | Per-directory `CLAUDE.md` table cells naming a bare filename absent from the directory subtree |
73
73
  | `conventional_pr_title_gate.py` | PreToolUse (Bash) | `gh pr create`/`gh pr edit` with a `--title` that is not a Conventional Commit, in a repo whose CI runs a semantic-pull-request title check |
74
+ | `cursor_cli_python_misfire_blocker.py` | PreToolUse (Bash/PowerShell) | `cursor` / `Cursor.exe` launched against a Python script with code-rules-gate flags, which raises Cursor's EPIPE unknown-option dialog |
74
75
  | `destructive_command_blocker.py` | PreToolUse (Bash/PowerShell) | Shell commands with destructive literals (`rm -rf`, `git reset --hard`, etc.) |
75
76
  | `docstring_rule_gate_count_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A stale spelled-out gate-validator count in `docstring-prose-matches-implementation.md` — the "N more gate validators" / "M gated slices" count drifting from the `check_docstring_*` validators the prose names |
76
77
  | `duplicate_rmtree_helper_blocker.py` | PreToolUse (Write/Edit) | A local re-definition of the Windows-safe rmtree helper trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`) in place of importing a shared helper |
@@ -94,7 +95,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
94
95
  | `precommit_code_rules_gate.py` | PreToolUse (Bash) | Staged changes that fail the CODE_RULES gate at commit time |
95
96
  | `pytest_testpaths_orphan_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | New `test_*.py` files created under a directory absent from a package's explicit pytest `testpaths` allowlist |
96
97
  | `question_to_user_enforcer.py` | Stop | User-directed questions not routed through `AskUserQuestion` |
97
- | `send_user_file_open_locally_blocker.py` | PreToolUse (SendUserFile) | A desk-side file attach (`SendUserFile` with `status` not `proactive`); points to opening the file with its native Windows app |
98
+ | `send_user_file_open_locally_blocker.py` | PreToolUse (SendUserFile) | A desk-side file attach (`SendUserFile` with `status` not `proactive`); points to `Invoke-Item -LiteralPath` for the native Windows app |
98
99
  | `sensitive_file_protector.py` | PreToolUse (Write/Edit/MultiEdit) | Writes to sensitive credential or config files |
99
100
  | `session_edit_stage_gate.py` | PreToolUse (Bash) | A `git commit` that would drop files edited this session because they are tracked but left unstaged |
100
101
  | `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook: deny Cursor launches that mistreat a Python gate script.
3
+
4
+ Agents sometimes run ``cursor code_rules_gate.py --base origin/main`` (or the
5
+ same shape through ``Cursor.exe`` / ``cursor.cmd``) when the intent is either
6
+ to execute the gate or to open a file. Cursor's main process then hits
7
+ ``onUnknownOption`` for the gate flags, ``console.warn`` writes to a closed
8
+ pipe, and Windows shows an EPIPE error dialog — while still opening the
9
+ ``.py`` path in the editor.
10
+
11
+ ::
12
+
13
+ cursor code_rules_gate.py --base origin/main flag
14
+ Cursor.exe gate.py --staged flag
15
+ cursor.cmd path/to/x.py --repo-root . flag
16
+ cursor code_rules_gate.py ok: editor open, no gate flags
17
+ cursor -g file.py:10 ok: editor goto, no gate flags
18
+ cursor README.md ok: open without gate flags
19
+ python code_rules_gate.py --base origin/main ok: real gate run
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
29
+ if _hooks_dir not in sys.path:
30
+ sys.path.insert(0, _hooks_dir)
31
+
32
+ from hooks_constants.cursor_cli_python_misfire_blocker_constants import ( # noqa: E402
33
+ ALL_SUPPORTED_TOOL_NAMES,
34
+ CALLING_HOOK_NAME,
35
+ COMMAND_KEY,
36
+ CORRECTIVE_MESSAGE,
37
+ CURSOR_LAUNCH_PATTERN,
38
+ DENY_DECISION,
39
+ GATE_FLAG_PATTERN,
40
+ GATE_SCRIPT_PATTERN,
41
+ HOOK_EVENT_NAME,
42
+ HOOK_EVENT_NAME_KEY,
43
+ HOOK_SPECIFIC_OUTPUT_KEY,
44
+ PERMISSION_DECISION_KEY,
45
+ PERMISSION_DECISION_REASON_KEY,
46
+ PYTHON_PATH_PATTERN,
47
+ TOOL_INPUT_KEY,
48
+ TOOL_NAME_KEY,
49
+ )
50
+ from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
51
+ from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
52
+ read_hook_input_dictionary_from_stdin,
53
+ )
54
+
55
+
56
+ def is_cursor_python_gate_misfire(command: str) -> bool:
57
+ """Return True when *command* launches Cursor against a gate-shaped Python run.
58
+
59
+ Args:
60
+ command: Raw Bash or PowerShell command string from the tool input.
61
+
62
+ Returns:
63
+ True when Cursor is the launch target, a gate CLI flag is present, and
64
+ the operands name the gate script or another ``.py`` path.
65
+ """
66
+ if not command or not CURSOR_LAUNCH_PATTERN.search(command):
67
+ return False
68
+ if not GATE_FLAG_PATTERN.search(command):
69
+ return False
70
+ return bool(GATE_SCRIPT_PATTERN.search(command) or PYTHON_PATH_PATTERN.search(command))
71
+
72
+
73
+ def main() -> None:
74
+ hook_input = read_hook_input_dictionary_from_stdin()
75
+ if hook_input is None:
76
+ sys.exit(0)
77
+
78
+ tool_name = hook_input.get(TOOL_NAME_KEY, "")
79
+ if tool_name not in ALL_SUPPORTED_TOOL_NAMES:
80
+ sys.exit(0)
81
+
82
+ tool_input = hook_input.get(TOOL_INPUT_KEY) or {}
83
+ if not isinstance(tool_input, dict):
84
+ sys.exit(0)
85
+ command = tool_input.get(COMMAND_KEY, "")
86
+ if not isinstance(command, str) or not is_cursor_python_gate_misfire(command):
87
+ sys.exit(0)
88
+
89
+ deny_payload = {
90
+ HOOK_SPECIFIC_OUTPUT_KEY: {
91
+ HOOK_EVENT_NAME_KEY: HOOK_EVENT_NAME,
92
+ PERMISSION_DECISION_KEY: DENY_DECISION,
93
+ PERMISSION_DECISION_REASON_KEY: CORRECTIVE_MESSAGE,
94
+ }
95
+ }
96
+ log_hook_block(
97
+ calling_hook_name=CALLING_HOOK_NAME,
98
+ hook_event=HOOK_EVENT_NAME,
99
+ block_reason=CORRECTIVE_MESSAGE,
100
+ tool_name=str(tool_name),
101
+ )
102
+ print(json.dumps(deny_payload))
103
+ sys.stdout.flush()
104
+ sys.exit(0)
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
@@ -496,13 +496,15 @@ def _emit_allow_decision(decision: DispatcherDecision) -> None:
496
496
  allow_payload: dict[str, object] = {
497
497
  "hookSpecificOutput": allow_hook_specific,
498
498
  }
499
- if decision.all_additional_context:
499
+ all_unique_additional_context = unique_first_seen_strings(decision.all_additional_context)
500
+ all_unique_system_messages = unique_first_seen_strings(decision.all_system_messages)
501
+ if all_unique_additional_context:
500
502
  allow_hook_specific["additionalContext"] = CONTEXT_JOIN_SEPARATOR.join(
501
- decision.all_additional_context
503
+ all_unique_additional_context
502
504
  )
503
- if decision.all_system_messages:
505
+ if all_unique_system_messages:
504
506
  allow_payload["systemMessage"] = SYSTEM_MESSAGE_JOIN_SEPARATOR.join(
505
- decision.all_system_messages
507
+ all_unique_system_messages
506
508
  )
507
509
  sys.stdout.write(json.dumps(allow_payload) + "\n")
508
510
  sys.stdout.flush()
@@ -3,8 +3,9 @@
3
3
 
4
4
  SendUserFile attaches a file to the session. While the user is at the terminal
5
5
  (status "normal" or unset) an attach does not let them see the file — it must
6
- open on screen in its native Windows app. The one attach allowed
7
- through is an away-from-desk phone push (status "proactive").
6
+ open on screen in its native Windows app. The corrective message names the
7
+ ``Invoke-Item -LiteralPath`` form. The one attach allowed through is an
8
+ away-from-desk phone push (status "proactive").
8
9
  """
9
10
 
10
11
  import json
@@ -219,6 +219,7 @@ def test_powershell_selects_the_shared_hooks_in_registration_order() -> None:
219
219
  for each_entry in select_applicable_entries(POWERSHELL_TOOL_NAME)
220
220
  ]
221
221
  assert powershell_paths == [
222
+ "blocking/cursor_cli_python_misfire_blocker.py",
222
223
  "blocking/unscoped_search_blocker.py",
223
224
  "blocking/pii_prevention_blocker.py",
224
225
  ]
@@ -0,0 +1,208 @@
1
+ """Behavior tests for the Cursor-vs-Python gate misfire PreToolUse hook."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import io
7
+ import json
8
+ import pathlib
9
+ import sys
10
+ from unittest import mock
11
+
12
+ _HOOK_DIR = pathlib.Path(__file__).parent
13
+ if str(_HOOK_DIR) not in sys.path:
14
+ sys.path.insert(0, str(_HOOK_DIR))
15
+
16
+ _HOOKS_DIR = str(_HOOK_DIR.parent)
17
+ if _HOOKS_DIR not in sys.path:
18
+ sys.path.insert(0, _HOOKS_DIR)
19
+
20
+ hook_spec = importlib.util.spec_from_file_location(
21
+ "cursor_cli_python_misfire_blocker",
22
+ _HOOK_DIR / "cursor_cli_python_misfire_blocker.py",
23
+ )
24
+ assert hook_spec is not None
25
+ assert hook_spec.loader is not None
26
+ hook_module = importlib.util.module_from_spec(hook_spec)
27
+ hook_spec.loader.exec_module(hook_module)
28
+
29
+ is_cursor_python_gate_misfire = hook_module.is_cursor_python_gate_misfire
30
+
31
+ from hooks_constants.cursor_cli_python_misfire_blocker_constants import ( # noqa: E402
32
+ CORRECTIVE_MESSAGE,
33
+ DENY_DECISION,
34
+ )
35
+
36
+
37
+ def test_blocks_cursor_with_code_rules_gate_and_base_flag() -> None:
38
+ assert (
39
+ is_cursor_python_gate_misfire(
40
+ "cursor code_rules_gate.py --base origin/main"
41
+ )
42
+ is True
43
+ )
44
+
45
+
46
+ def test_blocks_cursor_exe_with_staged_flag_on_python_path() -> None:
47
+ assert (
48
+ is_cursor_python_gate_misfire(r"Cursor.exe C:\temp\helper.py --staged")
49
+ is True
50
+ )
51
+
52
+
53
+ def test_blocks_cursor_cmd_with_repo_root_flag() -> None:
54
+ assert (
55
+ is_cursor_python_gate_misfire("cursor.cmd path/to/module.py --repo-root .")
56
+ is True
57
+ )
58
+
59
+
60
+ def test_allows_code_rules_gate_path_without_extra_flags() -> None:
61
+ assert is_cursor_python_gate_misfire("cursor code_rules_gate.py") is False
62
+
63
+
64
+ def test_allows_cursor_goto_on_python_file() -> None:
65
+ assert is_cursor_python_gate_misfire("cursor -g file.py:10") is False
66
+
67
+
68
+ def test_allows_cursor_goto_on_code_rules_gate_without_gate_flags() -> None:
69
+ assert (
70
+ is_cursor_python_gate_misfire("cursor -g code_rules_gate.py:40") is False
71
+ )
72
+
73
+
74
+ def test_blocks_start_process_cursor_with_quoted_gate_flag() -> None:
75
+ command = (
76
+ r'Start-Process Cursor.exe -ArgumentList '
77
+ r'"C:\temp\helper.py","--staged"'
78
+ )
79
+ assert is_cursor_python_gate_misfire(command) is True
80
+
81
+
82
+ def test_blocks_quoted_full_path_cursor_exe_with_gate_flags() -> None:
83
+ command = (
84
+ r"& 'C:\Program Files\Cursor\Cursor.exe' "
85
+ r"code_rules_gate.py --base origin/main"
86
+ )
87
+ assert is_cursor_python_gate_misfire(command) is True
88
+
89
+
90
+ def test_blocks_unquoted_absolute_cursor_exe_with_gate_flags() -> None:
91
+ command = (
92
+ r"C:\Apps\Cursor\Cursor.exe code_rules_gate.py --base origin/main"
93
+ )
94
+ assert is_cursor_python_gate_misfire(command) is True
95
+
96
+
97
+ def test_allows_relative_path_under_cursor_directory() -> None:
98
+ assert (
99
+ is_cursor_python_gate_misfire(
100
+ r"cursor\fix\x\code_rules_gate.py --base origin/main"
101
+ )
102
+ is False
103
+ )
104
+ assert (
105
+ is_cursor_python_gate_misfire(
106
+ "cursor/fix/x/code_rules_gate.py --base origin/main"
107
+ )
108
+ is False
109
+ )
110
+
111
+
112
+ def test_allows_cursor_open_of_non_python_file() -> None:
113
+ assert is_cursor_python_gate_misfire("cursor README.md") is False
114
+
115
+
116
+ def test_allows_python_running_the_gate() -> None:
117
+ assert (
118
+ is_cursor_python_gate_misfire(
119
+ "python code_rules_gate.py --base origin/main"
120
+ )
121
+ is False
122
+ )
123
+
124
+
125
+ def test_allows_invoke_item() -> None:
126
+ assert (
127
+ is_cursor_python_gate_misfire("Invoke-Item -LiteralPath 'C:\\tmp\\a.py'")
128
+ is False
129
+ )
130
+
131
+
132
+ def test_allows_empty_command() -> None:
133
+ assert is_cursor_python_gate_misfire("") is False
134
+
135
+
136
+ def test_corrective_message_names_python_and_invoke_item() -> None:
137
+ assert "python" in CORRECTIVE_MESSAGE
138
+ assert "Invoke-Item -LiteralPath" in CORRECTIVE_MESSAGE
139
+ assert "EPIPE" in CORRECTIVE_MESSAGE
140
+
141
+
142
+ def _run_main_with_io(input_text: str) -> str:
143
+ with mock.patch("sys.stdin", io.StringIO(input_text)):
144
+ with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
145
+ try:
146
+ hook_module.main()
147
+ except SystemExit:
148
+ pass
149
+ return mock_stdout.getvalue()
150
+
151
+
152
+ def test_main_denies_bash_misfire() -> None:
153
+ hook_input = {
154
+ "tool_name": "Bash",
155
+ "tool_input": {
156
+ "command": "cursor code_rules_gate.py --base origin/main",
157
+ },
158
+ }
159
+ output_text = _run_main_with_io(json.dumps(hook_input))
160
+ output = json.loads(output_text)
161
+ assert output["hookSpecificOutput"]["permissionDecision"] == DENY_DECISION
162
+ assert "cursor-python-misfire" in output["hookSpecificOutput"][
163
+ "permissionDecisionReason"
164
+ ]
165
+
166
+
167
+ def test_main_denies_powershell_misfire() -> None:
168
+ hook_input = {
169
+ "tool_name": "PowerShell",
170
+ "tool_input": {
171
+ "command": "cursor.cmd code_rules_gate.py --staged",
172
+ },
173
+ }
174
+ output_text = _run_main_with_io(json.dumps(hook_input))
175
+ output = json.loads(output_text)
176
+ assert output["hookSpecificOutput"]["permissionDecision"] == DENY_DECISION
177
+
178
+
179
+ def test_main_passes_python_gate_run() -> None:
180
+ hook_input = {
181
+ "tool_name": "Bash",
182
+ "tool_input": {
183
+ "command": "python code_rules_gate.py --base origin/main",
184
+ },
185
+ }
186
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
187
+
188
+
189
+ def test_main_passes_wrong_tool_name() -> None:
190
+ hook_input = {
191
+ "tool_name": "Write",
192
+ "tool_input": {
193
+ "command": "cursor code_rules_gate.py --base origin/main",
194
+ },
195
+ }
196
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
197
+
198
+
199
+ def test_main_passes_malformed_json() -> None:
200
+ assert _run_main_with_io("not valid json {{{") == ""
201
+
202
+
203
+ def test_main_passes_when_tool_input_is_null() -> None:
204
+ hook_input = {
205
+ "tool_name": "Bash",
206
+ "tool_input": None,
207
+ }
208
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
@@ -47,7 +47,10 @@ def test_allows_proactive_status() -> None:
47
47
 
48
48
  def test_corrective_message_points_to_native_windows_app() -> None:
49
49
  assert "native Windows app" in CORRECTIVE_MESSAGE
50
+ assert "Invoke-Item -LiteralPath" in CORRECTIVE_MESSAGE
50
51
  assert "Show-Asset.ps1" not in CORRECTIVE_MESSAGE
52
+ assert "Cursor.exe" in CORRECTIVE_MESSAGE
53
+ assert "EPIPE" in CORRECTIVE_MESSAGE
51
54
 
52
55
 
53
56
  def test_corrective_message_names_proactive_escape_hatch() -> None:
@@ -21,6 +21,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
21
21
  | `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
22
22
  | `command_dispatch_constants.py` | Command-word regex, command-key access pattern, tokenization pattern, and anchors for the unanchored command-dispatch meta-gate |
23
23
  | `conventional_pr_title_gate_constants.py` | Bash tool name, gh executable basenames and pr create/edit subcommand tokens, title/repo flag names, semantic-title-CI workflow markers, the Conventional Commits type list and title pattern, the semantic-pull-request action `types:` input pattern, and block-message text for the conventional-PR-title gate |
24
+ | `cursor_cli_python_misfire_blocker_constants.py` | Detection patterns and deny message for Cursor launches that mistreat a Python code-rules-gate script |
24
25
  | `dead_argparse_argument_constants.py` | Patterns for detecting unused argparse arguments |
25
26
  | `dead_config_field_constants.py` | Patterns for detecting unused `*Config` / `*Selectors` dataclass fields |
26
27
  | `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
@@ -63,7 +64,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
63
64
  | `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
64
65
  | `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
65
66
  | `ruff_integration_constants.py` | ``NO_COLOR`` / ``FORCE_COLOR`` environment variable names for plain ruff diagnostics |
66
- | `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
67
+ | `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the `Invoke-Item` block message for the open-locally attach blocker |
67
68
  | `sensitive_file_protector_constants.py` | Sensitive filename patterns, committed-template suffixes that earn an exemption, and the deny decision and message template for `sensitive_file_protector` |
68
69
  | `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
69
70
  | `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
@@ -58,6 +58,10 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
58
58
  BashHostedHookEntry("blocking/gh_body_arg_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
59
59
  BashHostedHookEntry("blocking/shell_substitution_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
60
60
  BashHostedHookEntry("blocking/piped_pytest_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
61
+ BashHostedHookEntry(
62
+ "blocking/cursor_cli_python_misfire_blocker.py",
63
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
64
+ ),
61
65
  BashHostedHookEntry(
62
66
  "blocking/unscoped_search_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
63
67
  ),
@@ -0,0 +1,79 @@
1
+ """Constants for the Cursor-vs-Python gate misfire PreToolUse blocker.
2
+
3
+ Blocks Shell commands that launch Cursor's CLI or GUI binary against a Python
4
+ script with code-rules-gate flags. That path feeds unknown options into
5
+ Cursor's main process ``resolveArgs``, where ``console.warn`` on a closed pipe
6
+ raises an EPIPE error dialog and still opens the script as an editor path.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+
13
+ __all__ = [
14
+ "ALL_SUPPORTED_TOOL_NAMES",
15
+ "BASH_TOOL_NAME",
16
+ "POWERSHELL_TOOL_NAME",
17
+ "TOOL_NAME_KEY",
18
+ "TOOL_INPUT_KEY",
19
+ "COMMAND_KEY",
20
+ "HOOK_SPECIFIC_OUTPUT_KEY",
21
+ "HOOK_EVENT_NAME_KEY",
22
+ "HOOK_EVENT_NAME",
23
+ "PERMISSION_DECISION_KEY",
24
+ "DENY_DECISION",
25
+ "PERMISSION_DECISION_REASON_KEY",
26
+ "CALLING_HOOK_NAME",
27
+ "CURSOR_LAUNCH_PATTERN",
28
+ "GATE_SCRIPT_PATTERN",
29
+ "GATE_FLAG_PATTERN",
30
+ "PYTHON_PATH_PATTERN",
31
+ "CORRECTIVE_MESSAGE",
32
+ ]
33
+
34
+ BASH_TOOL_NAME = "Bash"
35
+ POWERSHELL_TOOL_NAME = "PowerShell"
36
+ ALL_SUPPORTED_TOOL_NAMES: frozenset[str] = frozenset(
37
+ {BASH_TOOL_NAME, POWERSHELL_TOOL_NAME}
38
+ )
39
+
40
+ TOOL_NAME_KEY = "tool_name"
41
+ TOOL_INPUT_KEY = "tool_input"
42
+ COMMAND_KEY = "command"
43
+
44
+ HOOK_SPECIFIC_OUTPUT_KEY = "hookSpecificOutput"
45
+ HOOK_EVENT_NAME_KEY = "hookEventName"
46
+ HOOK_EVENT_NAME = "PreToolUse"
47
+ PERMISSION_DECISION_KEY = "permissionDecision"
48
+ DENY_DECISION = "deny"
49
+ PERMISSION_DECISION_REASON_KEY = "permissionDecisionReason"
50
+
51
+ CALLING_HOOK_NAME = "cursor_cli_python_misfire_blocker.py"
52
+
53
+ CURSOR_LAUNCH_PATTERN = re.compile(
54
+ r"(?i)"
55
+ r"(?:"
56
+ r"(?:^|[\s;&|])(?:cursor(?:\.cmd|\.exe)?)(?=\s|[\"']|$)"
57
+ r"|"
58
+ r"[\\/](?:cursor(?:\.cmd|\.exe)?)(?=\s|[\"']|$)"
59
+ r")"
60
+ )
61
+ GATE_SCRIPT_PATTERN = re.compile(r"(?i)code_rules_gate\.py\b")
62
+ GATE_FLAG_PATTERN = re.compile(
63
+ r"(?i)(?:^|[\s,\"'])(?:--base|--staged|--repo-root|--only-under)"
64
+ r"(?:\s|=|$|[\"'])"
65
+ )
66
+ PYTHON_PATH_PATTERN = re.compile(r"(?i)\.py(?:\s|$|['\":])")
67
+
68
+ CORRECTIVE_MESSAGE = (
69
+ "BLOCKED [cursor-python-misfire]: Cursor's CLI/GUI was invoked against a "
70
+ "Python script with code-rules-gate flags. Cursor treats those flags as "
71
+ "unknown options, warns on a closed pipe, and raises an EPIPE error dialog "
72
+ "while still opening the script as an editor path.\n\n"
73
+ "To run the gate:\n"
74
+ " python <path-to>/code_rules_gate.py --base origin/main\n\n"
75
+ "To open a file on screen in its native Windows app:\n"
76
+ " Invoke-Item -LiteralPath '<path>'\n\n"
77
+ "Do not pass --base, --staged, --repo-root, or --only-under to cursor / "
78
+ "Cursor.exe."
79
+ )
@@ -7,7 +7,11 @@ PROACTIVE_STATUS: str = "proactive"
7
7
  CORRECTIVE_MESSAGE: str = (
8
8
  "BLOCKED [open-locally]: SendUserFile attaches a file to the session, which "
9
9
  "does not let the user see it while they are at the terminal. Open the file on "
10
- "screen with its native Windows app. Open every path the user named.\n"
10
+ "screen with its native Windows app:\n"
11
+ " Invoke-Item -LiteralPath '<path>'\n"
12
+ "Open every path the user named. Do not launch Cursor.exe or cursor.cmd to open "
13
+ "a file — unknown flags on that path raise an EPIPE error dialog in Cursor's "
14
+ "main process.\n"
11
15
  "The one allowed attach is a phone push: when the user has stepped away and you "
12
16
  'want the file to reach their phone, call SendUserFile with status "proactive".'
13
17
  )
@@ -27,6 +27,7 @@ _EXPECTED_BASH_ORDER = (
27
27
  "blocking/gh_body_arg_blocker.py",
28
28
  "blocking/shell_substitution_blocker.py",
29
29
  "blocking/piped_pytest_blocker.py",
30
+ "blocking/cursor_cli_python_misfire_blocker.py",
30
31
  "blocking/unscoped_search_blocker.py",
31
32
  "blocking/nas_ssh_binary_enforcer.py",
32
33
  "blocking/volatile_path_in_post_blocker.py",
@@ -41,6 +42,7 @@ _EXPECTED_BASH_ORDER = (
41
42
  )
42
43
 
43
44
  _POWERSHELL_APPLICABLE = (
45
+ "blocking/cursor_cli_python_misfire_blocker.py",
44
46
  "blocking/unscoped_search_blocker.py",
45
47
  "blocking/pii_prevention_blocker.py",
46
48
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.21.1",
3
+ "version": "2.21.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {