Hook trong harness

Hook

Bốn hook đăng ký ở harness/.claude/settings.json, riêng git guard đăng ký thêm ở harness/.codex/hooks.json. Nguyên tắc chung của cả bốn là hook chỉ làm lớp thứ hai, còn contract là lớp thứ nhất. Luật nào quan trọng thì phải nằm trong contract, vì contract được đọc ở mọi runtime, còn hook thì có thể bị tắt, có thể chưa được trust, và có thể treo trên một đường thi hành không ai đi qua.

Trường hợp cuối không phải giả thuyết. Một trong bốn hook dưới đây đang ngủ, tôi đo được điều đó, và tôi để nguyên nó ở đây thay vì lặng lẽ gỡ đi.

PreToolUse · Bash

scripts/hook-git-guard.py

Bắn khi
Trước mỗi lệnh Bash, lúc quyền còn chưa được quyết.
Chặn hay ghi
Tách argv ra rồi từ chối lệnh git phá việc của người khác. Nó chỉ từ chối, không bao giờ tự chạy gì.
Cổng Gác Trước Mọi Lệnh Bash A workflow diagram generated by Archify. 01 / Mỗi Lệnh Bash quyết định của git guard Lệnh Bash · bất kỳ tool call nào · Mỗi Lệnh Bash Lệnh Bash bất kỳ tool call nào PreToolUse · bắn trước khi gọi · Mỗi Lệnh Bash PreToolUse bắn trước khi gọi hook-git-guard.py · allow hay block? · Mỗi Lệnh Bash › quyết định của git guard hook-git-guard.py allow hay block? Block · stderr + đã log, không chạy · Mỗi Lệnh Bash › quyết định của git guard Block stderr + đã log, không chạy Lệnh chạy · giữ nguyên · Mỗi Lệnh Bash › quyết định của git guard Lệnh chạy giữ nguyên chạy script guard pattern nguy hiểm sạch Legend User UI Agent logic Policy
Explore ↗mở tab mới · giao diện tiếng Anh

Hook bắn trước mỗi lần một agent định chạy tool Bash, trên cả Claude Code lẫn Codex, và nó chạy scripts/hook-git-guard.py.

Script đọc lệnh sắp chạy rồi làm đúng một trong hai việc: từ chối kèm câu lệnh chính xác cần chạy thay thế, hoặc không nói gì. Nó không bao giờ tự thi hành. Bản đầu tiên có chạy git worktree prune và giết broker từ trong hook, và đó là sai về nguyên tắc. Hook PreToolUse chạy trong lúc quyền còn đang được quyết, nên mọi thứ nó sửa là tác dụng phụ của một lệnh có thể vẫn bị từ chối ngay sau đó.

Script tách token và soi argv chứ không match regex trên chuỗi lệnh thô. Một lượt cross-vendor chứng minh bản regex vừa cho lọt vừa quá tay trong cùng một hơi thở: /usr/bin/git add -Agit -c k=v add -A đi qua được, trong khi printf '%s' "rm -rf .claude/worktrees/x" bị chặn chỉ vì chứa mấy chữ đó. Một guard vừa trượt trường hợp thật vừa chặn trường hợp vô hại thì dạy chính người dùng nó cách đi vòng, và như vậy tệ hơn không có guard.

Tôi muốn nói chính xác về giới hạn của nó. Đây là lint chống nhầm tay, không phải hàng rào. Ba lượt gate đối kháng, mỗi lượt tìm ra một đường mới đi xuyên qua nó, và lượt thứ ba kết luận matcher này không hội tụ. Câu trả lời là thu nhỏ tuyên bố lại chứ không phải thêm luật. Bây giờ nó nhận đúng một hình dạng, là các lệnh đơn ngăn nhau bằng toán tử không nằm trong ngoặc, và im lặng trước mọi thứ có substitution, heredoc, comment, từ khoá, wrapper hay interpreter. Im lặng ở đó là thiết kế, không phải lỗ hổng: một tuyên bố phủ sóng không đúng sự thật còn tệ hơn không tuyên bố gì.

Còn một lý do nó là file .py chứ không phải một dòng shell nhét trong settings.json. Bản cũ đúng là một dòng như vậy: không đọc được, không chạy tay được, không test được, và nó nằm ngủ qua nhiều release trong lúc trông vẫn như đã cài (AST-102). Bản này chạy độc lập được, nên nó kiểm tra được.

harness/scripts/hook-git-guard.py ↗AST-102WorktreeRemove hook does not fire, but documentation declares manual cleanup redundant

WorktreeRemove

scripts/release-worktree-resources.sh

Bắn khi
Khi một worktree bị gỡ.
Chặn hay ghi
Reap các tiến trình mọc trong worktree, rồi gọi plug riêng của project nếu có, và ghi một dòng vào log hook.
Dọn Worktree Khi Tháo Gỡ A workflow diagram generated by Archify. 01 / Dọn Worktree WorktreeRemove · bắn khi tháo worktree · Dọn Worktree WorktreeRemove bắn khi tháo worktree release-worktree-resources.sh · giải phóng pane + process · Dọn Worktree release-worktree-resources.sh giải phóng pane + process chạy script release Legend Agent logic
Explore ↗mở tab mới · giao diện tiếng Anh

Hook bắn khi một worktree bị gỡ qua đường tool EnterWorktree / ExitWorktree, và nó chạy scripts/release-worktree-resources.sh với $WORKTREE_PATH.

Việc của script là giải phóng theo đúng thứ tự những gì một worktree đã cấp phát. Trước hết là phần harness biết: tiến trình có cwd thật nằm trong worktree, do reap-worktree-processes.sh reap. Sau đó là phần project tự khai, qua plug .astraler/project/cleanup-worktree.sh. Thứ tự này chịu lực. Tài nguyên buộc vào một thư mục, theo cwd hoặc theo label suy ra từ đường dẫn hoặc theo cái tên project tự tính từ nó, không còn khớp được sau khi thư mục biến mất, nên bước này phải chạy trước git worktree remove, không bao giờ sau (AST-100, AST-101).

Phần tiếp theo tôi phải nói thẳng: hook này đang ngủ. Đo ngày 2026-08-20 bằng chính log tôi thêm vào để trả lời câu hỏi đó. Ba worktree bị gỡ sau lần ghi cuối của log, trong đó có một lệnh git worktree remove trần, và số sự kiện WorktreeRemove ghi được là không. Trong khi đó hook SubagentStop nằm cùng file, cùng session, ghi được 27 sự kiện trong cùng cửa sổ thời gian ấy. Tôi kiểm lại theo một đường độc lập: container test dùng chung vẫn Up (healthy) sau lần gỡ, mà một hook còn sống thì đã dừng nó.

Nguyên nhân không phải hook hỏng, mà là hook không được chạm tới. WorktreeRemove treo trên đường tool EnterWorktree / ExitWorktree, còn Thomas gỡ worktree bằng git worktree remove trong một lệnh Bash. Đó là git thuần, không có gì đứng giữa lệnh và repo, nên không có sự kiện nào để bắn. Một hook treo trên đường thi hành không ai đi qua thì bắn đúng bằng một hook hỏng, và nhìn từ bên ngoài hai thứ đó không phân biệt được.

Vì vậy bước dọn thủ công vẫn bắt buộc trên mọi runtime, và lệnh trong hook vẫn phải giữ an toàn cho ngày nó tỉnh dậy (AST-115). Bài học sống lâu hơn chính cái hook này: khi một cơ chế không bắn, hãy hỏi trigger có được chạm tới không, trước khi kết luận cơ chế đã hỏng.

harness/scripts/release-worktree-resources.sh ↗AST-100Codex companion broker leaks one process per arm pass, accumulating silently

SessionStart · compact

scripts/hook-contract-reload.py

Bắn khi
Ngay sau khi context bị nén.
Chặn hay ghi
Nạp lại contract của vai, vì bản đọc bằng Read là thứ bị nén đi trước nhất.
Tiếp Tục Session Sau Compact A workflow diagram generated by Archify. 01 / Tiếp Tục Session SessionStart · matcher: compact · Tiếp Tục Session SessionStart matcher: compact hook-contract-reload.py · nạp lại contract của vai · Tiếp Tục Session hook-contract-reload.py nạp lại contract của vai nạp lại contract Legend Agent logic
Explore ↗mở tab mới · giao diện tiếng Anh

Hook bắn ngay sau khi một session Claude Code compact, với source: compact, và nó chạy scripts/hook-contract-reload.py.

Script nạp lại đúng một thứ: đường dẫn contract của vai đang chạy, qua hookSpecificOutput.additionalContext, tới tay agent trước khi agent kịp hành động.

Lỗi sinh ra nó là AST-069, và dạng một dòng là: một chỉ dẫn không gắn với khoảnh khắc nào thì đo được bằng không. System prompt đã có sẵn dòng “Read .agents/roles/<role>.md now”, và dòng đó đi qua compaction nguyên vẹn vì nó là system prompt. Thứ không đi qua được là chữ “now”. Một agent vừa compact đọc bản tóm tắt của chính nó, thấy công việc đang dở, kết luận nó đang giữa session, và không bao giờ đọc lại. Chỉ dẫn vẫn nằm đó, và trơ.

Hai chi tiết là cố ý. Hook bắn ở compact chứ không ở clear, vì sau /clear agent đối diện một context rỗng và tự đọc contract, còn sau compaction nó đối diện một bản tóm tắt khẳng định việc đang chạy nên nó không đọc. Chính sự bất đối xứng đó là toàn bộ defect. Và hook chỉ có ở Claude Code, vì Codex với OpenCode không có compaction, nên ở đó không có khoảnh khắc nào để gắn vào.

Mọi nhánh thoát của script hoặc in ra JSON hợp lệ hoặc không in gì, và không nhánh nào ném exception. Một traceback trên stderr sẽ bị runtime đọc là hook hỏng rồi tắt đi, và người vận hành sẽ tiếp tục ship một harness trông như đã lên đạn.

harness/scripts/hook-contract-reload.py ↗AST-069An instruction with no moment attached measures zero

SubagentStop

settings.json

Bắn khi
Khi một subagent dừng.
Chặn hay ghi
Ghi loại agent, id và session vào /tmp/harness-hook-events.log. Không chặn gì cả.
Ghi Log Mọi Lần Subagent Thoát A workflow diagram generated by Archify. 01 / Subagent Thoát SubagentStop · bắn mỗi khi agent thoát · Subagent Thoát SubagentStop bắn mỗi khi agent thoát harness-hook-events.log · ghi lại lần thoát · Subagent Thoát harness-hook-events.log ghi lại lần thoát ghi thêm một dòng log Legend Agent logic Context / trace
Explore ↗mở tab mới · giao diện tiếng Anh

Hook bắn mỗi lần một subagent kết thúc. Không có script riêng: hook đọc JSON từ stdin và ghi thêm một dòng vào /tmp/harness-hook-events.log.

Nó ghi agent_type, agent_id, session_id và kích thước payload. Chỉ vậy, và nó không chặn gì. Bản đầu đọc biến môi trường $AGENT_NAME$SESSION_ID rồi nhận về chuỗi rỗng. Bản hiện tại đọc stdin và lấy đúng trường.

Đây là hook rẻ nhất trong bốn cái, và nó trả lại giá trị lớn nhất ở một chỗ không ai thiết kế trước. Nó là nhóm đối chứng. 27 sự kiện nó ghi được trong cùng cửa sổ thời gian là thứ biến “không quan sát được gì” thành bằng chứng cho việc WorktreeRemove đang ngủ. Không có chúng thì quan sát đó không phân biệt được với “toàn bộ hook đã bị tắt”, và kết luận đầu tiên, rằng hook đã hỏng, được rút ra khi chưa có nhóm đối chứng nào.

Ngoài vai trò đó, hook còn ghi lại lúc một Builder chết, thứ mà nếu không có log thì chỉ hiện ra dưới dạng một pane im lặng.

AST-102WorktreeRemove hook does not fire, but documentation declares manual cleanup redundant