Skip to content

[Bug]: PreToolUse hooks are skipped for subagent tool calls (PostToolUse still runs) #35

Open

Description

@tosin2013

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

  • PreToolUse runs only in chat/tools.rs → process_tool_calls_once() (the run_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 reaches process_tool_calls_once().
  • PostToolUse runs in execute_single_tool() (around line 3234), which both paths reach. That's why the two events disagree.

Reproduce

  1. Project .refact/hooks.yaml, with the project listed in hooks.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'"
  2. In a task (planner → card → task agent), let the task agent call subagent with a goal that edits a file.
  3. The task agent's own apply_patch is blocked (Blocked by hook: blocked-by-test-hook). The subagent's apply_patch and shell calls execute, and the file changes in its worktree.

Observed (trajectory excerpts, one task)

  • agent-T-1-*: apply_patch → Blocked by hook: …, then subagent {"auto_merge": true, …}.
  • subchat-81a5911b-*: apply_patch → [diff] (applied, no hook), a second apply_patch → applied, and shell npm run test:media ran. A PreToolUse hook 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_patch calls 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

  1. tosin2013 commented on Sep 26, 2026

    @tosin2013
    Author

    Tracking on our side: tosin2013/repo-governor#254. We'll re-run the same scenario against a fix and report back here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions