本章介紹如何從零開始建立一個 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  # 總是返回 0

Q: 如何調試 Hook?

  1. 使用日誌:
1logger = setup_hook_logging("my_hook", level=logging.DEBUG)
2logger.debug(f"輸入資料: {hook_input}")
  1. 查看日誌檔案:
1tail -f .claude/hook-logs/my_hook.log

Q: Hook 執行順序是什麼?

同一事件的多個 Hook 按照 settings.json 中定義的順序執行。

思考題

  1. 為什麼要將 Hook 邏輯封裝在 main() 函式中?
  2. 什麼時候應該使用 block,什麼時候使用 allow
  3. 如何設計 Hook 以便於測試?

下一章:如何擴展共用模組 回到首頁:Python 維護工程師實戰指南