6.1 如何新增一個 Hook
6.1 如何新增一個 Hook
本章介紹如何從零開始建立一個 Claude Code Hook 腳本。這是一個實戰指南,整合了前面學到的所有概念。
前置知識
建議先閱讀:
Hook 系統概述
Claude Code Hook 是在特定事件發生時執行的腳本,例如:
SessionStart- 會話開始Stop- Claude 主動結束PreToolUse- 工具使用前PostToolUse- 工具使用後
步驟 1:建立基本結構
使用 UV 單檔模式
推薦使用 uv 的單檔腳本模式:
1#!/usr/bin/env -S uv run --quiet --script
2# /// script
3# requires-python = ">=3.10"
4# dependencies = []
5# ///
6"""
7My Custom Hook - 簡短描述
8
9Hook Event: SessionStart
10
11這裡寫詳細說明。
12"""
13
14import sys
15from pathlib import Path
16
17# 添加 lib 目錄到路徑
18sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
19
20from hook_logging import setup_hook_logging
21
22def main():
23 """Hook 主函式"""
24 logger = setup_hook_logging("my_custom_hook")
25 logger.info("Hook 開始執行")
26
27 # 你的邏輯在這裡
28
29 logger.info("Hook 執行完成")
30 return 0
31
32if __name__ == "__main__":
33 sys.exit(main())路徑設定
Hook 腳本需要能找到共用模組:
1import sys
2from pathlib import Path
3
4# 添加 lib 目錄到路徑
5sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
6
7# 現在可以導入共用模組
8from git_utils import get_current_branch
9from hook_logging import setup_hook_logging
10from hook_io import read_hook_input, write_hook_output步驟 2:處理輸入輸出
SessionStart Hook 範例
SessionStart Hook 不需要讀取輸入,直接輸出即可:
1#!/usr/bin/env -S uv run --quiet --script
2# /// script
3# requires-python = ">=3.10"
4# dependencies = []
5# ///
6"""
7Branch Status Reminder - 分支狀態提醒
8
9Hook Event: SessionStart
10"""
11
12import sys
13from pathlib import Path
14
15sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
16
17from git_utils import (
18 get_current_branch,
19 get_project_root,
20 is_protected_branch,
21)
22
23
24def main():
25 print("=" * 60)
26 print("Branch Status Reminder")
27 print("=" * 60)
28
29 current_branch = get_current_branch()
30 if not current_branch:
31 print("警告: 無法獲取分支資訊")
32 return 0
33
34 is_protected = is_protected_branch(current_branch)
35 branch_status = "保護分支" if is_protected else "開發分支"
36
37 print(f"當前分支: {current_branch} ({branch_status})")
38 print(f"工作目錄: {get_project_root()}")
39
40 if is_protected:
41 print()
42 print("警告: 當前在保護分支上")
43 print("建議: 建立 feature 分支後再進行開發")
44
45 print("=" * 60)
46 return 0
47
48
49if __name__ == "__main__":
50 sys.exit(main())PreToolUse/PostToolUse Hook 範例
這類 Hook 需要讀取 stdin 並輸出 JSON:
1#!/usr/bin/env -S uv run --quiet --script
2# /// script
3# requires-python = ">=3.10"
4# dependencies = []
5# ///
6"""
7Tool Usage Logger - 記錄工具使用情況
8
9Hook Event: PostToolUse
10"""
11
12import sys
13from pathlib import Path
14
15sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
16
17from hook_io import read_hook_input, write_hook_output
18from hook_logging import setup_hook_logging
19
20
21def main():
22 logger = setup_hook_logging("tool_usage_logger")
23
24 try:
25 # 讀取 Hook 輸入
26 hook_input = read_hook_input()
27
28 if hook_input is None:
29 write_hook_output(continue_execution=True)
30 return 0
31
32 # 取得工具資訊
33 tool_name = hook_input.get("tool_name", "unknown")
34 tool_input = hook_input.get("tool_input", {})
35
36 # 記錄使用情況
37 logger.info(f"工具使用: {tool_name}")
38 logger.debug(f"輸入參數: {tool_input}")
39
40 # 輸出結果(不阻擋執行)
41 write_hook_output(continue_execution=True)
42
43 except Exception as e:
44 logger.error(f"Hook 執行錯誤: {e}")
45 write_hook_output(continue_execution=True)
46
47 return 0
48
49
50if __name__ == "__main__":
51 sys.exit(main())步驟 3:使用共用模組
Git 工具
1from git_utils import (
2 run_git_command,
3 get_current_branch,
4 get_project_root,
5 is_protected_branch,
6 is_allowed_branch,
7)
8
9# 取得當前分支
10branch = get_current_branch()
11
12# 檢查是否為保護分支
13if is_protected_branch(branch):
14 print("警告:你在保護分支上")
15
16# 執行 git 命令
17success, output = run_git_command(["status", "--short"])
18if success:
19 print(output)日誌系統
1from hook_logging import setup_hook_logging
2
3# 設定日誌
4logger = setup_hook_logging("my_hook")
5
6# 使用日誌
7logger.debug("詳細資訊")
8logger.info("一般資訊")
9logger.warning("警告訊息")
10logger.error("錯誤訊息")輸入輸出
1from hook_io import (
2 read_hook_input,
3 write_hook_output,
4 create_pretooluse_output,
5 create_posttooluse_output,
6)
7
8# 讀取輸入
9hook_input = read_hook_input()
10
11# 允許繼續執行
12write_hook_output(continue_execution=True)
13
14# 阻擋執行並附帶訊息
15write_hook_output(
16 continue_execution=False,
17 decision="block",
18 reason="不允許此操作"
19)
20
21# PreToolUse 專用輸出
22output = create_pretooluse_output(
23 decision="allow", # 或 "block", "modify"
24 reason="操作允許"
25)
26print(json.dumps(output))步驟 4:註冊 Hook
在 .claude/settings.json 中註冊:
1{
2 "hooks": {
3 "SessionStart": [
4 {
5 "type": "command",
6 "command": "python .claude/hooks/branch-status-reminder.py",
7 "event": "SessionStart"
8 }
9 ],
10 "PreToolUse": [
11 {
12 "type": "command",
13 "command": "python .claude/hooks/tool-guard.py",
14 "event": "PreToolUse"
15 }
16 ]
17 }
18}步驟 5:撰寫測試
1# tests/test_my_hook.py
2"""
3My Hook 測試
4"""
5
6import unittest
7from unittest.mock import patch, MagicMock
8import sys
9from pathlib import Path
10
11# 添加 hooks 目錄到路徑
12sys.path.insert(0, str(Path(__file__).parent.parent / ".claude" / "hooks"))
13
14
15class TestMyHook(unittest.TestCase):
16 """測試 My Hook"""
17
18 @patch("my_hook.get_current_branch")
19 def test_protected_branch_warning(self, mock_branch):
20 """測試保護分支警告"""
21 mock_branch.return_value = "main"
22
23 # 導入並測試
24 from my_hook import check_branch_status
25
26 result = check_branch_status()
27 self.assertTrue(result["is_protected"])
28
29 @patch("my_hook.read_hook_input")
30 def test_empty_input_handling(self, mock_input):
31 """測試空輸入處理"""
32 mock_input.return_value = None
33
34 from my_hook import main
35
36 # 應該正常退出,不拋出異常
37 result = main()
38 self.assertEqual(result, 0)
39
40
41if __name__ == "__main__":
42 unittest.main()完整範例:檔案類型檢查 Hook
1#!/usr/bin/env -S uv run --quiet --script
2# /// script
3# requires-python = ">=3.10"
4# dependencies = []
5# ///
6"""
7File Type Permission Hook - 檔案類型權限檢查
8
9阻止對特定類型檔案的危險操作。
10
11Hook Event: PreToolUse
12"""
13
14import sys
15from pathlib import Path
16
17sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
18
19from hook_io import read_hook_input, create_pretooluse_output
20from hook_logging import setup_hook_logging
21import json
22
23
24# 配置:受保護的檔案模式
25PROTECTED_PATTERNS = [
26 "*.env",
27 "*.pem",
28 "*.key",
29 "*credentials*",
30]
31
32# 需要檢查的工具
33MONITORED_TOOLS = ["Write", "Edit", "Bash"]
34
35
36def main():
37 logger = setup_hook_logging("file_type_permission")
38
39 try:
40 hook_input = read_hook_input()
41
42 if hook_input is None:
43 print(json.dumps(create_pretooluse_output("allow")))
44 return 0
45
46 tool_name = hook_input.get("tool_name", "")
47 tool_input = hook_input.get("tool_input", {})
48
49 # 只檢查特定工具
50 if tool_name not in MONITORED_TOOLS:
51 print(json.dumps(create_pretooluse_output("allow")))
52 return 0
53
54 # 取得檔案路徑
55 file_path = tool_input.get("file_path") or tool_input.get("command", "")
56
57 # 檢查是否為受保護的檔案
58 if is_protected_file(file_path):
59 logger.warning(f"阻擋對受保護檔案的操作: {file_path}")
60 output = create_pretooluse_output(
61 decision="block",
62 reason=f"此檔案受保護,不允許直接操作: {file_path}"
63 )
64 else:
65 output = create_pretooluse_output("allow")
66
67 print(json.dumps(output))
68
69 except Exception as e:
70 logger.error(f"Hook 錯誤: {e}")
71 print(json.dumps(create_pretooluse_output("allow")))
72
73 return 0
74
75
76def is_protected_file(file_path: str) -> bool:
77 """檢查檔案是否受保護"""
78 if not file_path:
79 return False
80
81 path = Path(file_path)
82
83 for pattern in PROTECTED_PATTERNS:
84 if path.match(pattern):
85 return True
86
87 return False
88
89
90if __name__ == "__main__":
91 sys.exit(main())開發檢查清單
新增 Hook 時的檢查項目:
- 使用 UV 單檔模式(
#!/usr/bin/env -S uv run --quiet --script) - 正確設定
sys.path以導入共用模組 - 使用
setup_hook_logging()設定日誌 - 使用
read_hook_input()/write_hook_output()處理 I/O - 妥善處理異常(不讓 Hook 崩潰影響系統)
- 在
settings.json中註冊 - 撰寫單元測試
- 更新文件
常見問題
Q: Hook 執行失敗會影響 Claude 嗎?
Hook 應該優雅地處理錯誤,不要讓異常傳播出去:
1def main():
2 try:
3 # 你的邏輯
4 pass
5 except Exception as e:
6 logger.error(f"Error: {e}")
7 # 允許繼續執行,不阻擋系統
8 write_hook_output(continue_execution=True)
9
10 return 0 # 總是返回 0Q: 如何調試 Hook?
- 使用日誌:
1logger = setup_hook_logging("my_hook", level=logging.DEBUG)
2logger.debug(f"輸入資料: {hook_input}")- 查看日誌檔案:
1tail -f .claude/hook-logs/my_hook.logQ: Hook 執行順序是什麼?
同一事件的多個 Hook 按照 settings.json 中定義的順序執行。
思考題
- 為什麼要將 Hook 邏輯封裝在
main()函式中? - 什麼時候應該使用
block,什麼時候使用allow? - 如何設計 Hook 以便於測試?
下一章:如何擴展共用模組 回到首頁:Python 維護工程師實戰指南