Repository navigation
[Bug]: PreToolUse hooks are skipped for subagent tool calls (PostToolUse still runs) #35
Description
Summary
PreToolUse hooks are not run for tool calls made by a background subagent (the subagent tool) while it runs in its subchat loop. A hook that blocks a tool with exit 2 is honoured in normal chat sessions, but a subagent can make the same call and it executes. PostToolUse does run for those calls, so they are observed but never gated.
Any hook used as a guard (a policy check, a "no git push" rule, a governance gate) can be bypassed by delegating the work to a subagent. It can happen without intent: in our run, the blocked agent's very next step was to hand the task to a subagent with auto_merge: true.
Version
refact 8.6.4 (binary commit 8e61d80); source read at main 0d096c9. macOS arm64.
Where it happens
PreToolUseruns only inchat/tools.rs→process_tool_calls_once()(therun_hooks(app, HookEvent::PreToolUse, …)call around line 2306).- A subagent's loop runs tools through
subchat.rs→execute_pending_tool_calls()→execute_tools()(around line 2975), which never reachesprocess_tool_calls_once(). PostToolUseruns inexecute_single_tool()(around line 3234), which both paths reach. That's why the two events disagree.
Reproduce
- Project
.refact/hooks.yaml, with the project listed inhooks.trusted_projects:hooks: PreToolUse: - matcher: "^(apply_patch|create_textdoc|update_textdoc.*|shell)$" hooks: - type: command command: "sh -c 'echo blocked-by-test-hook >&2; exit 2'"
- In a task (planner → card → task agent), let the task agent call
subagentwith a goal that edits a file. - The task agent's own
apply_patchis blocked (Blocked by hook: blocked-by-test-hook). The subagent'sapply_patchandshellcalls execute, and the file changes in its worktree.
Observed (trajectory excerpts, one task)
agent-T-1-*:apply_patch→Blocked by hook: …, thensubagent {"auto_merge": true, …}.subchat-81a5911b-*:apply_patch→[diff](applied, no hook), a secondapply_patch→ applied, andshell npm run test:mediaran. APreToolUsehook on^shell$that logs every command logged nothing.- The same subagent, after the goal monitor resumed it ("Answer interrupted by an urgent message from chat.goal_monitor"), made its next
apply_patchcalls through the session path, and those were blocked. So one subagent's calls were gated or ungated depending on which loop happened to execute them.
Suggested fix
Run PreToolUse in the path both loops share, or call it from execute_pending_tool_calls() before execute_tools(), and drop blocked calls with the same Blocked by hook: <reason> tool message the session path produces. Moving it next to PostToolUse in execute_single_tool() would keep the two events symmetric.
Context
Found while integrating Repo Governor, which uses Refact's hooks as a governance gate (tracking: tosin2013/repo-governor, issue linked below). Happy to test a fix against the same scenario.
Activity
tosin2013 commentedon Sep 26, 2026
Tracking on our side: tosin2013/repo-governor#254. We'll re-run the same scenario against a fix and report back here.
A runbook for governing a repository's Refact agent (issue 247) (#255)