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.
|
|
18
|
-
child joins into one
|
|
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
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
305
|
-
|
|
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:
|