claude-dev-env 8.46.11 → 8.46.13

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.
@@ -435,7 +435,9 @@ def is_under_session_scratchpad(file_path: str, hook_payload: dict) -> bool:
435
435
  """Return True when file_path resolves under the harness session scratchpad.
436
436
 
437
437
  One-off scripts written to the session scratchpad are throwaway tooling
438
- outside every repo, so the TDD and CODE_RULES gates skip them.
438
+ outside every repo, so the TDD and CODE_RULES gates skip them. A path at
439
+ or under the payload's working directory (``cwd``) is never scratch, so a
440
+ repository worktree checked out inside the scratchpad stays fully gated.
439
441
 
440
442
  The match keys on the session id — from the payload, or the
441
443
  ``CLAUDE_CODE_SESSION_ID`` environment variable — and on the temp-directory
@@ -446,10 +448,12 @@ def is_under_session_scratchpad(file_path: str, hook_payload: dict) -> bool:
446
448
 
447
449
  Args:
448
450
  file_path: The path the write targets.
449
- hook_payload: The PreToolUse payload carrying the session id.
451
+ hook_payload: The PreToolUse payload carrying the session id and the
452
+ working directory (``cwd``).
450
453
 
451
454
  Returns:
452
- True when file_path's real path sits at or under the session scratchpad.
455
+ True when file_path's real path sits at or under the session scratchpad
456
+ and outside the payload's working directory.
453
457
  """
454
458
  if not file_path:
455
459
  return False
@@ -457,6 +461,12 @@ def is_under_session_scratchpad(file_path: str, hook_payload: dict) -> bool:
457
461
  if not session_id:
458
462
  return False
459
463
  real_target = os.path.realpath(file_path)
464
+ working_directory = str(hook_payload.get("cwd") or "")
465
+ if working_directory and _is_at_or_under_root(
466
+ _normalized_absolute_comparison_path(real_target),
467
+ _normalized_absolute_comparison_path(os.path.realpath(working_directory)),
468
+ ):
469
+ return False
460
470
  real_temp_root = os.path.realpath(tempfile.gettempdir())
461
471
  all_relative_parts = _relative_parts_under_temp_root(real_target, real_temp_root)
462
472
  return (
@@ -262,10 +262,12 @@ def test_is_ephemeral_path_false_for_repository_file(
262
262
  ) -> None:
263
263
  _simulate_windows_platform(monkeypatch, tmp_path)
264
264
  monkeypatch.delenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", raising=False)
265
- repository_file = tmp_path / "repository" / "orders.py"
266
- repository_file.parent.mkdir(parents=True)
265
+ repository_file_outside_temp_root = Path(WORKING_DIRECTORY).parent / "repository" / "orders.py"
267
266
 
268
- assert _SHARED_MODULE.is_ephemeral_path(str(repository_file), _session_payload()) is False
267
+ assert (
268
+ _SHARED_MODULE.is_ephemeral_path(str(repository_file_outside_temp_root), _session_payload())
269
+ is False
270
+ )
269
271
 
270
272
 
271
273
  def test_is_ephemeral_path_reads_session_id_from_environment(
@@ -309,3 +311,49 @@ def test_normalized_path_marker_table_carries_no_backslash_entry(
309
311
  not the marker, so a backslash entry can never match."""
310
312
  marker_table = getattr(_SHARED_MODULE, normalized_marker_table_name)
311
313
  assert all("\\" not in each_marker for each_marker in marker_table)
314
+
315
+
316
+ def test_ephemeral_path_is_false_for_file_under_payload_cwd_inside_scratchpad(
317
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
318
+ ) -> None:
319
+ _point_temporary_directory_at(monkeypatch, tmp_path)
320
+ scratchpad_directory = _build_scratchpad_directory(tmp_path)
321
+ repository_module = scratchpad_directory / "orders.py"
322
+ payload_with_scratchpad_cwd = {"cwd": str(scratchpad_directory), "session_id": SESSION_ID}
323
+
324
+ assert (
325
+ _SHARED_MODULE.is_ephemeral_path(str(repository_module), payload_with_scratchpad_cwd)
326
+ is False
327
+ )
328
+
329
+
330
+ def test_scratchpad_is_false_for_worktree_under_payload_cwd(
331
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
332
+ ) -> None:
333
+ _point_temporary_directory_at(monkeypatch, tmp_path)
334
+ scratchpad_directory = _build_scratchpad_directory(tmp_path)
335
+ worktree_directory = scratchpad_directory / "worktree"
336
+ repository_module = worktree_directory / "src" / "orders.py"
337
+ payload_with_worktree_cwd = {"cwd": str(worktree_directory), "session_id": SESSION_ID}
338
+
339
+ assert (
340
+ _SHARED_MODULE.is_under_session_scratchpad(str(repository_module), payload_with_worktree_cwd)
341
+ is False
342
+ )
343
+
344
+
345
+ def test_ephemeral_path_stays_true_for_scratchpad_file_outside_payload_cwd(
346
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
347
+ ) -> None:
348
+ _point_temporary_directory_at(monkeypatch, tmp_path)
349
+ scratchpad_directory = _build_scratchpad_directory(tmp_path)
350
+ throwaway_script = scratchpad_directory / "one_off_tool.py"
351
+ payload_with_worktree_cwd = {
352
+ "cwd": str(scratchpad_directory / "worktree"),
353
+ "session_id": SESSION_ID,
354
+ }
355
+
356
+ assert _SHARED_MODULE.is_ephemeral_path(str(throwaway_script), _session_payload()) is True
357
+ assert (
358
+ _SHARED_MODULE.is_ephemeral_path(str(throwaway_script), payload_with_worktree_cwd) is True
359
+ )
@@ -11,7 +11,7 @@ USAGE_PACE_SCRIPT_FILE_NAME = "usage_pace.py"
11
11
  USAGE_PACE_TIMEOUT_SECONDS = 15
12
12
 
13
13
  TOOL_INPUT_KEY = "tool_input"
14
- THREAD_MODEL_WHEN_OVER_PACE = "opus"
14
+ THREAD_MODEL_WHEN_OVER_PACE = "claude-opus-5-5"
15
15
  THREAD_EFFORT_WHEN_OVER_PACE = "low"
16
16
  MODEL_INPUT_KEY = "model"
17
17
  EFFORT_INPUT_KEY = "effort"
@@ -22,7 +22,7 @@ ADDITIONAL_CONTEXT_KEY = "additionalContext"
22
22
  NOT_AN_OBJECT_REASON = "start_thread_session input is not a JSON object"
23
23
  RESHAPE_CONTEXT_PREFIX = (
24
24
  "Usage over pace or unreadable: this thread spawn now runs on "
25
- "opus at low effort. Pace verdict: "
25
+ "claude-opus-5-5 at low effort. Pace verdict: "
26
26
  )
27
27
  PACE_VERDICT_CONTEXT_MAXIMUM_CHARACTERS = 600
28
28
  LAUNCH_FAILURE_ERROR_TEMPLATE = "usage pace did not finish: {failure_type}"
@@ -54,7 +54,7 @@ def _assert_reshaped(stdout: str) -> None:
54
54
  assert "permissionDecision" not in decision
55
55
  assert decision["updatedInput"] == {
56
56
  **SPAWN_INPUT,
57
- "model": "opus",
57
+ "model": "claude-opus-5-5",
58
58
  "effort": "low",
59
59
  }
60
60
 
@@ -8,7 +8,7 @@ beside it, or the script ``COORDINATOR_USAGE_PACE_SCRIPT`` names.
8
8
 
9
9
  usage under pace (exit 1) -> no output; the call runs unchanged
10
10
  over pace (exit 0) or unreadable -> updatedInput:
11
- model "opus", effort "low"
11
+ model "claude-opus-5-5", effort "low"
12
12
  input that cannot be reshaped -> deny with a one-line reason
13
13
 
14
14
  ``updatedInput`` is the whole tool input the call runs with, so it carries
@@ -65,13 +65,13 @@ class SpawnNotReshapable(Exception):
65
65
 
66
66
 
67
67
  def reshape_thread_spawn(tool_input: object) -> dict[str, object]:
68
- """Move a thread spawn to the latest Opus at low effort.
68
+ """Move a thread spawn to Opus 5.5 at low effort.
69
69
 
70
70
  ::
71
71
 
72
72
  {"title": "t", "instructions": "Do X."}
73
73
  -> {"title": "t", "instructions": "Do X.",
74
- "model": "opus", "effort": "low"}
74
+ "model": "claude-opus-5-5", "effort": "low"}
75
75
 
76
76
  Args:
77
77
  tool_input: The ``tool_input`` object of the start_thread_session call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.46.11",
3
+ "version": "8.46.13",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {