claude-dev-env 8.61.0 → 8.61.1

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.
@@ -14,8 +14,9 @@ guards then never run. This hook runs them::
14
14
  Each child hook receives the same stdin payload, runs inside its checkout, and
15
15
  keeps its own matcher and timeout. A child ``exit 2`` blocks the tool call and a
16
16
  child ``deny`` or ``ask`` decision passes through; a child ``allow`` is dropped,
17
- so no child widens permissions for the session. SessionStart output from every
18
- child joins into one ``additionalContext``.
17
+ so no child widens permissions for the session. The ``additionalContext`` of
18
+ every child, at SessionStart and at PreToolUse, joins into one
19
+ ``additionalContext``.
19
20
 
20
21
  When the session directory is itself a checkout or holds its own settings,
21
22
  Claude Code already runs those hooks, and this hook exits at once.
@@ -285,33 +286,58 @@ def pre_tool_use_verdict(
285
286
 
286
287
  Returns:
287
288
  A blocking exit carrying the child's stderr when any child exits 2,
288
- else the first child ``deny`` or ``ask`` output, else an empty pass.
289
+ else the first child ``deny`` or ``ask`` output carrying one copy
290
+ of each distinct child ``additionalContext``, else a pass carrying
291
+ that context, else an empty pass.
289
292
  """
290
293
  for each_result in all_results:
291
294
  if each_result.returncode == BLOCKING_EXIT_CODE:
292
295
  return HookVerdict(BLOCKING_EXIT_CODE, "", each_result.stderr)
293
- for each_result in all_results:
294
- if each_result.returncode != 0:
295
- continue
296
- parsed_output = _parsed_hook_output(each_result.stdout)
297
- if parsed_output is None:
298
- continue
299
- if _permission_decision(parsed_output) in ALL_FORWARDED_PERMISSION_DECISIONS:
300
- return HookVerdict(0, json.dumps(parsed_output), "")
301
- return HookVerdict(0, "", "")
296
+ all_parsed_outputs = [
297
+ _parsed_hook_output(each_result.stdout)
298
+ for each_result in all_results
299
+ if each_result.returncode == 0
300
+ ]
301
+ all_parsed_outputs = [each_output for each_output in all_parsed_outputs if each_output is not None]
302
+ all_context_sections = _non_empty_sections(
303
+ _json_additional_context(each_parsed_output) for each_parsed_output in all_parsed_outputs
304
+ )
305
+ all_distinct_context_sections = list(dict.fromkeys(all_context_sections))
306
+ joined_context = CONTEXT_SECTION_SEPARATOR.join(all_distinct_context_sections)
307
+ for each_parsed_output in all_parsed_outputs:
308
+ if _permission_decision(each_parsed_output) in ALL_FORWARDED_PERMISSION_DECISIONS:
309
+ decision_output = dict(each_parsed_output)
310
+ if joined_context:
311
+ decision_specific_output = dict(decision_output[HOOK_SPECIFIC_OUTPUT_KEY])
312
+ decision_specific_output[ADDITIONAL_CONTEXT_KEY] = joined_context
313
+ decision_output[HOOK_SPECIFIC_OUTPUT_KEY] = decision_specific_output
314
+ return HookVerdict(0, json.dumps(decision_output), "")
315
+ if not joined_context:
316
+ return HookVerdict(0, "", "")
317
+ hook_output = {
318
+ HOOK_SPECIFIC_OUTPUT_KEY: {
319
+ HOOK_EVENT_NAME_OUTPUT_KEY: PRE_TOOL_USE_EVENT,
320
+ ADDITIONAL_CONTEXT_KEY: joined_context,
321
+ }
322
+ }
323
+ return HookVerdict(0, json.dumps(hook_output), "")
302
324
 
303
325
 
304
- def _session_start_context(stdout_text: str) -> str:
305
- parsed_output = _parsed_hook_output(stdout_text)
306
- if parsed_output is None:
307
- return stdout_text.strip()
308
- specific_output = parsed_output.get(HOOK_SPECIFIC_OUTPUT_KEY)
326
+ def _json_additional_context(all_output_fields: dict[str, object]) -> str:
327
+ specific_output = all_output_fields.get(HOOK_SPECIFIC_OUTPUT_KEY)
309
328
  if not isinstance(specific_output, dict):
310
329
  return ""
311
330
  context_text = specific_output.get(ADDITIONAL_CONTEXT_KEY)
312
331
  return context_text.strip() if isinstance(context_text, str) else ""
313
332
 
314
333
 
334
+ def _session_start_context(stdout_text: str) -> str:
335
+ parsed_output = _parsed_hook_output(stdout_text)
336
+ if parsed_output is None:
337
+ return stdout_text.strip()
338
+ return _json_additional_context(parsed_output)
339
+
340
+
315
341
  def _non_empty_sections(all_sections: Iterable[str]) -> list[str]:
316
342
  return [each_section for each_section in all_sections if each_section]
317
343
 
@@ -178,6 +178,95 @@ def test_should_not_forward_a_child_allow_decision(tmp_path: pathlib.Path) -> No
178
178
  assert completed_process.stdout.strip() == ""
179
179
 
180
180
 
181
+ def test_should_carry_child_pre_tool_use_context_without_its_allow(
182
+ tmp_path: pathlib.Path,
183
+ ) -> None:
184
+ context_output = {
185
+ "hookSpecificOutput": {
186
+ "hookEventName": "PreToolUse",
187
+ "permissionDecision": "allow",
188
+ "additionalContext": "Build this page from the house template.",
189
+ }
190
+ }
191
+ _make_checkout(
192
+ tmp_path,
193
+ "alpha",
194
+ {"PreToolUse": [_command_group("Artifact", f"echo '{json.dumps(context_output)}'")]},
195
+ )
196
+
197
+ completed_process = _run_hook(tmp_path, _pre_tool_use_payload("Artifact"))
198
+
199
+ assert completed_process.returncode == 0
200
+ assert json.loads(completed_process.stdout) == {
201
+ "hookSpecificOutput": {
202
+ "hookEventName": "PreToolUse",
203
+ "additionalContext": "Build this page from the house template.",
204
+ }
205
+ }
206
+
207
+
208
+ def test_should_carry_another_child_context_into_an_ask_decision(
209
+ tmp_path: pathlib.Path,
210
+ ) -> None:
211
+ context_output = {
212
+ "hookSpecificOutput": {
213
+ "hookEventName": "PreToolUse",
214
+ "additionalContext": "Build this page from the house template.",
215
+ }
216
+ }
217
+ ask_output = {
218
+ "hookSpecificOutput": {
219
+ "hookEventName": "PreToolUse",
220
+ "permissionDecision": "ask",
221
+ "permissionDecisionReason": "publishing needs a person",
222
+ }
223
+ }
224
+ _make_checkout(
225
+ tmp_path,
226
+ "alpha",
227
+ {"PreToolUse": [_command_group("Artifact", f"echo '{json.dumps(context_output)}'")]},
228
+ )
229
+ _make_checkout(
230
+ tmp_path,
231
+ "beta",
232
+ {"PreToolUse": [_command_group("Artifact", f"echo '{json.dumps(ask_output)}'")]},
233
+ )
234
+
235
+ completed_process = _run_hook(tmp_path, _pre_tool_use_payload("Artifact"))
236
+
237
+ assert completed_process.returncode == 0
238
+ assert json.loads(completed_process.stdout) == {
239
+ "hookSpecificOutput": {
240
+ "hookEventName": "PreToolUse",
241
+ "permissionDecision": "ask",
242
+ "permissionDecisionReason": "publishing needs a person",
243
+ "additionalContext": "Build this page from the house template.",
244
+ }
245
+ }
246
+
247
+
248
+ def test_should_carry_one_copy_of_context_two_checkouts_repeat(
249
+ tmp_path: pathlib.Path,
250
+ ) -> None:
251
+ context_output = {
252
+ "hookSpecificOutput": {
253
+ "hookEventName": "PreToolUse",
254
+ "additionalContext": "Build this page from the house template.",
255
+ }
256
+ }
257
+ for each_checkout_name in ("alpha", "beta"):
258
+ _make_checkout(
259
+ tmp_path,
260
+ each_checkout_name,
261
+ {"PreToolUse": [_command_group("Artifact", f"echo '{json.dumps(context_output)}'")]},
262
+ )
263
+
264
+ completed_process = _run_hook(tmp_path, _pre_tool_use_payload("Artifact"))
265
+
266
+ assert completed_process.returncode == 0
267
+ assert json.loads(completed_process.stdout) == context_output
268
+
269
+
181
270
  def test_should_skip_a_child_hook_whose_matcher_names_another_tool(
182
271
  tmp_path: pathlib.Path,
183
272
  ) -> None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.61.0",
3
+ "version": "8.61.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {