claude-dev-env 8.54.5 → 8.54.6

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.
@@ -11,7 +11,8 @@ The hook stays silent when:
11
11
  - the stop is already a retry after a block (stop_hook_active is true);
12
12
  - the session shows no title tool: no title tool named anywhere in the
13
13
  transcript, and either no remote session id in the environment or a
14
- deferred tool listing that leaves the title tool out;
14
+ deferred tool listing that leaves the title tool out, or an earlier
15
+ Stop block the session never met;
15
16
  - the transcript cannot be read.
16
17
  """
17
18
 
@@ -32,6 +33,7 @@ from hooks_constants.session_title_constants import (
32
33
  ALLOW_EXIT_CODE,
33
34
  BLOCK_DECISION,
34
35
  DEFERRED_TOOL_ATTACHMENT_TYPES,
36
+ HOOK_BLOCKING_ERROR_ATTACHMENT_TYPE,
35
37
  REMOTE_SESSION_ENVIRONMENT_VARIABLE,
36
38
  REMOTE_TITLE_TOOL_NAME,
37
39
  STOP_BLOCK_REASON,
@@ -85,6 +87,15 @@ def _lists_deferred_tools(all_entry_fields: dict[str, object]) -> bool:
85
87
  return isinstance(attachment, dict) and attachment.get("type") in DEFERRED_TOOL_ATTACHMENT_TYPES
86
88
 
87
89
 
90
+ def _is_stop_block(all_entry_fields: dict[str, object]) -> bool:
91
+ attachment = all_entry_fields.get("attachment")
92
+ return (
93
+ isinstance(attachment, dict)
94
+ and attachment.get("type") == HOOK_BLOCKING_ERROR_ATTACHMENT_TYPE
95
+ and attachment.get("hookEvent") == STOP_GATE_EVENT_NAME
96
+ )
97
+
98
+
88
99
  def _is_title_call(entry_type: object, all_block_fields: dict[str, object]) -> bool:
89
100
  return (
90
101
  entry_type == "assistant"
@@ -132,6 +143,7 @@ def stop_block_reason(all_entries: list[dict[str, object]], is_remote_session: b
132
143
  prompt -> work, no title call => block reason
133
144
  no title tool anywhere, not remote => None
134
145
  remote, tool list lacks the title tool => None
146
+ remote, earlier Stop block never met => None
135
147
 
136
148
  Args:
137
149
  all_entries: The transcript entries, oldest first.
@@ -144,7 +156,8 @@ def stop_block_reason(all_entries: list[dict[str, object]], is_remote_session: b
144
156
  each_name for each_entry in all_entries for each_name in _title_tool_names_in(each_entry)
145
157
  ]
146
158
  if not all_seen_tool_names and (
147
- not is_remote_session or any(_lists_deferred_tools(each) for each in all_entries)
159
+ not is_remote_session
160
+ or any(_lists_deferred_tools(each) or _is_stop_block(each) for each in all_entries)
148
161
  ):
149
162
  return None
150
163
  if _title_set_since(all_entries[_turn_start_index(all_entries) :]):
@@ -199,6 +199,43 @@ def test_should_block_a_remote_session_whose_tool_list_names_the_title_tool(
199
199
  assert json.loads(stdout_text)["decision"] == "block"
200
200
 
201
201
 
202
+ def earlier_stop_block() -> dict[str, object]:
203
+ return {
204
+ "type": "attachment",
205
+ "attachment": {"type": "hook_blocking_error", "hookEvent": "Stop"},
206
+ }
207
+
208
+
209
+ def test_should_stay_silent_in_a_remote_session_after_an_unmet_earlier_block(
210
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
211
+ ) -> None:
212
+ monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
213
+ all_entries = [
214
+ prompt("Fix the runner"),
215
+ earlier_stop_block(),
216
+ prompt("Next task"),
217
+ tool_call("t1", "Bash"),
218
+ tool_result("t1"),
219
+ ]
220
+ assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
221
+
222
+
223
+ def test_should_block_after_an_earlier_block_once_the_title_tool_is_listed(
224
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
225
+ ) -> None:
226
+ monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
227
+ all_entries = [
228
+ deferred_tools(REMOTE_TITLE_TOOL),
229
+ prompt("Fix the runner"),
230
+ earlier_stop_block(),
231
+ prompt("Next task"),
232
+ tool_call("t1", "Bash"),
233
+ tool_result("t1"),
234
+ ]
235
+ _, stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)
236
+ assert json.loads(stdout_text)["decision"] == "block"
237
+
238
+
202
239
  def test_should_stay_silent_on_the_retry_after_a_block(
203
240
  monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
204
241
  ) -> None:
@@ -15,6 +15,7 @@ ALLOW_EXIT_CODE = 0
15
15
  BLOCK_EXIT_CODE = 2
16
16
  BLOCK_DECISION = "block"
17
17
  DEFERRED_TOOL_ATTACHMENT_TYPES = frozenset({"deferred_tools_delta", "deferred_tools_record"})
18
+ HOOK_BLOCKING_ERROR_ATTACHMENT_TYPE = "hook_blocking_error"
18
19
 
19
20
  STATUS_PREFIX_PATTERN = re.compile("^(\U0001f6a9|\u2705|\u23f3)\ufe0f? (?=\\S)")
20
21
  MAXIMUM_NAME_LENGTH = 25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.54.5",
3
+ "version": "8.54.6",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {