对应 hooks.rs:152-200 的 run_command() 完整流程: 启动子进程 → stdin 传 JSON → 读 stdout → 解析退出码
(
script: str,
event: str,
tool_name: str,
tool_input: str,
payload: str,
)
| 613 | return self.denied |
| 614 | |
| 615 | def run_single_hook( |
| 616 | script: str, |
| 617 | event: str, |
| 618 | tool_name: str, |
| 619 | tool_input: str, |
| 620 | payload: str, |
| 621 | ) -> HookCommandOutcome: |
| 622 | """ |
| 623 | 对应 hooks.rs:152-200 的 run_command() |
| 624 | 完整流程: 启动子进程 → stdin 传 JSON → 读 stdout → 解析退出码 |
| 625 | """ |
| 626 | # 构造子进程环境变量 (hooks.rs:166-172) |
| 627 | env = os.environ.copy() |
| 628 | env["HOOK_EVENT"] = event |
| 629 | env["HOOK_TOOL_NAME"] = tool_name |
| 630 | env["HOOK_TOOL_INPUT"] = tool_input |
| 631 | env["HOOK_TOOL_IS_ERROR"] = "0" |
| 632 | |
| 633 | try: |
| 634 | result = subprocess.run( |
| 635 | ["python3", "-c", script], |
| 636 | input=payload, # stdin 传 JSON (hooks.rs:174) |
| 637 | capture_output=True, # stdout + stderr (hooks.rs:163-165) |
| 638 | text=True, |
| 639 | env=env, # 环境变量 (hooks.rs:166-172) |
| 640 | timeout=5, # 防止 hook 挂死 |
| 641 | ) |
| 642 | |
| 643 | stdout = result.stdout.strip() |
| 644 | message = stdout if stdout else None |
| 645 | |
| 646 | # 退出码协议 (hooks.rs:179-196) |
| 647 | if result.returncode == 0: |
| 648 | return HookCommandOutcome("Allow", message) |
| 649 | elif result.returncode == 2: |
| 650 | return HookCommandOutcome( |
| 651 | "Deny", |
| 652 | message or f"Hook denied tool `{tool_name}`", |
| 653 | ) |
| 654 | else: |
| 655 | return HookCommandOutcome( |
| 656 | "Warn", |
| 657 | f"Hook exited with code {result.returncode}: {stdout or result.stderr.strip()}", |
| 658 | ) |
| 659 | |
| 660 | except subprocess.TimeoutExpired: |
| 661 | return HookCommandOutcome("Warn", "Hook 超时被杀死") |
| 662 | except Exception as e: |
| 663 | return HookCommandOutcome("Warn", f"Hook 执行失败: {e}") |
| 664 | |
| 665 | def run_hooks( |
| 666 | hooks: list[str], |
no test coverage detected