claude-dev-env 8.54.4 → 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.4",
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": {
@@ -15,9 +15,32 @@ is in place by the time any test module runs its top-level imports.
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import importlib
18
19
  import sys
19
20
  from pathlib import Path
20
21
 
22
+ import pytest
23
+
21
24
  _scripts_directory = str(Path(__file__).resolve().parent)
22
25
  if _scripts_directory not in sys.path:
23
26
  sys.path.insert(0, _scripts_directory)
27
+
28
+ _live_broker_state_resolver = importlib.import_module("account_broker_support").broker_state_path
29
+
30
+
31
+ @pytest.fixture(autouse=True)
32
+ def isolated_broker_state_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
33
+ """Point every module's ``broker_state_path`` at a per-test file.
34
+
35
+ ``account_broker`` binds ``broker_state_path`` by name from
36
+ ``account_broker_support``, so each bound copy is replaced. A test that
37
+ patches only one copy still keeps the other off the home directory.
38
+
39
+ Returns:
40
+ The isolated broker state path for this test.
41
+ """
42
+ isolated_path = tmp_path / "account-broker" / "state.json"
43
+ for each_module in list(sys.modules.values()):
44
+ if getattr(each_module, "broker_state_path", None) is _live_broker_state_resolver:
45
+ monkeypatch.setattr(each_module, "broker_state_path", lambda: isolated_path)
46
+ return isolated_path
@@ -0,0 +1,48 @@
1
+ """Broker state written during a test stays out of the home directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ import account_broker
12
+ from dev_env_scripts_constants.account_broker_constants import (
13
+ Account,
14
+ Meters,
15
+ Product,
16
+ ProductAdapter,
17
+ )
18
+
19
+
20
+ def test_meter_cache_write_lands_outside_the_home_directory(
21
+ monkeypatch: pytest.MonkeyPatch,
22
+ tmp_path: Path,
23
+ isolated_broker_state_path: Path,
24
+ ) -> None:
25
+ fake_home = tmp_path / "home"
26
+ monkeypatch.setenv("HOME", str(fake_home))
27
+ monkeypatch.setenv("USERPROFILE", str(fake_home))
28
+ account = Account(Product.CLAUDE, "only", tmp_path / "only")
29
+ reset_at = datetime(2026, 10, 5, tzinfo=timezone.utc)
30
+ adapter = ProductAdapter(
31
+ lambda: (account,),
32
+ lambda each_account: Meters(80, reset_at, 80, reset_at),
33
+ "CLAUDE_CONFIG_DIR",
34
+ ("usage limit",),
35
+ )
36
+ monkeypatch.setitem(account_broker.all_product_adapters, Product.CLAUDE, adapter)
37
+
38
+ def runner(argv: list[str], **options: object) -> subprocess.CompletedProcess[str]:
39
+ return subprocess.CompletedProcess(argv, 0, "", "")
40
+
41
+ with account_broker.override_subprocess_runner(runner):
42
+ account_broker.run_job(Product.CLAUDE, ["claude"])
43
+
44
+ home_state_path = fake_home / ".claude" / "account-broker" / "state.json"
45
+ assert not home_state_path.exists()
46
+ assert f"claude:only:{account.home}" in isolated_broker_state_path.read_text(
47
+ encoding="utf-8"
48
+ )
@@ -1129,7 +1129,6 @@ def test_usage_limit_fallover_delivers_full_prompt_to_each_account(
1129
1129
  return subprocess.CompletedProcess(argv, 0, FIXTURE_CLAUDE_STDOUT, "")
1130
1130
 
1131
1131
  monkeypatch.setitem(account_broker.all_product_adapters, Product.CLAUDE, adapter)
1132
- monkeypatch.setattr(account_broker, "broker_state_path", lambda: tmp_path / "state.json")
1133
1132
  monkeypatch.setattr(
1134
1133
  dispatcher,
1135
1134
  "spawn_preflight_runner",