Skip to content

Fix onPreToolUse and permission hooks not firing for sub-agent tool calls#89

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/fix-on-pre-tool-use-hook
Draft

Fix onPreToolUse and permission hooks not firing for sub-agent tool calls#89
Copilot wants to merge 3 commits intomainfrom
copilot/fix-on-pre-tool-use-hook

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 21, 2026

Fixes #64.

hooks.invoke and permission.request RPC calls from sub-agent sessions were silently dropped because the CLI creates sub-agent sessions internally with their own session IDs that are never registered in the SDK's session map — causing strict ID lookups to miss them entirely.


Before the change?

  • handleHooksInvoke returned a -32602 error for any session ID not in the SDK registry. Sub-agent hook invocations were silently lost.
  • handlePermissionRequest returned denied for unknown session IDs. Sub-agent permission checks fell through with no user-visible handler.
  • onPreToolUse / onPostToolUse hooks registered on the parent session were invisible to tools invoked by the CLI-managed sub-agent.

After the change?

  • RpcHandlerDispatcher: When a hooks.invoke or permission.request arrives with an unrecognized session ID, the dispatcher falls back to the first registered session that has the appropriate handler:

    • Hook fallback: returns null output (no-op) if no session has hooks — no error sent.
    • Permission fallback: returns denied if no session has a handler — same safe default, no -32602.
  • CopilotSession: Two new package-private query methods support the fallback lookup:

    • hasHooksHandler() — true if hooks with at least one handler are registered.
    • hasPermissionHandler() — true if a permission handler is registered.
  • RpcHandlerDispatcher: Two new private helpers, findSessionWithHooks() and findSessionWithPermissionHandler(), iterate registered sessions to find a suitable fallback. Selection is non-deterministic when multiple sessions have handlers (documented in Javadoc).

  • RpcHandlerDispatcherTest: Updated existing hooksInvokeWithUnknownSession test (renamed to hooksInvokeWithUnknownSessionAndNoFallback) and added four new unit tests covering both the fallback-succeeds and no-fallback-available scenarios for hooks and permissions.

Pull request checklist

  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been reviewed and added / updated if needed (for bug fixes / features)
  • mvn spotless:apply has been run to format the code
  • mvn clean verify passes locally

Does this introduce a breaking change?

  • Yes
  • No

Copilot AI changed the title [WIP] Fix onPreToolUse hook to fire for sub-agent tool calls Fix onPreToolUse and permission hooks not firing for sub-agent tool calls Apr 21, 2026
Copilot AI requested a review from edburns April 21, 2026 15:48
@edburns
Copy link
Copy Markdown
Collaborator

edburns commented Apr 21, 2026

I will mark this Ready for review to get Copilot's review on it, but then take it back to draft.

@edburns edburns marked this pull request as ready for review April 21, 2026 16:13
Copilot AI review requested due to automatic review settings April 21, 2026 16:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

Comments suppressed due to low confidence (3)

src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java:421

  • The fallback selection is explicitly non-deterministic when multiple sessions have permission handlers. For permission decisions this can produce inconsistent (and potentially unsafe) behavior if different sessions have different policies, since an “unknown” session could be approved/denied depending on map iteration order. Consider making the fallback deterministic or safer when ambiguous (e.g., only fall back when exactly one session has a handler; otherwise return denied and/or log a warning).
    /**
     * Finds the first registered session that has a permission handler registered.
     * <p>
     * Used as a fallback when the session ID in an incoming
     * {@code permission.request} request is not in the registry (e.g. sub-agent
     * sessions created internally by the CLI). When multiple sessions have
     * permission handlers registered, the selection is non-deterministic because
     * the underlying {@link java.util.concurrent.ConcurrentHashMap} does not
     * guarantee iteration order.
     *
     * @return a session with a permission handler, or {@code null} if none is found
     */
    private CopilotSession findSessionWithPermissionHandler() {
        for (CopilotSession s : sessions.values()) {
            if (s.hasPermissionHandler()) {
                return s;
            }
        }
        return null;

src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java:401

  • Similarly, hooks fallback is non-deterministic across multiple sessions with hooks registered, which can lead to surprising cross-session behavior (e.g., hooks from an unrelated session gating tool execution for a sub-agent). Consider applying the same “only if unambiguous” rule (exactly one session with hooks) or otherwise choosing a deterministic session selection strategy and logging when falling back.
    /**
     * Finds the first registered session that has a hooks handler registered.
     * <p>
     * Used as a fallback when the session ID in an incoming {@code hooks.invoke}
     * request is not in the registry (e.g. sub-agent sessions created internally by
     * the CLI). When multiple sessions have hooks registered, the selection is
     * non-deterministic because the underlying
     * {@link java.util.concurrent.ConcurrentHashMap} does not guarantee iteration
     * order.
     *
     * @return a session with hooks, or {@code null} if none is found
     */
    private CopilotSession findSessionWithHooks() {
        for (CopilotSession s : sessions.values()) {
            if (s.hasHooksHandler()) {
                return s;
            }
        }
        return null;
    }

src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java:305

  • Same as permission fallback: when an unknown/sub-agent sessionId is handled by a different (fallback) CopilotSession, the HookInvocation created inside CopilotSession.handleHooksInvoke() will report the fallback session’s sessionId rather than the originating request sessionId. If callers use invocation.getSessionId() for correlation or policy, this will be misleading. Consider allowing the dispatcher to pass the originating sessionId through to the HookInvocation (e.g., via an overload or parameter).
                CopilotSession session = sessions.get(sessionId);
                if (session == null) {
                    // Fall back to any registered session that has hooks registered.
                    // This handles sub-agent sessions created internally by the CLI whose IDs
                    // are not in the SDK's session registry.
                    session = findSessionWithHooks();
                }
  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment on lines +194 to 199
// Fall back to any registered session that has a permission handler.
// This handles sub-agent sessions created internally by the CLI whose IDs
// are not in the SDK's session registry.
session = findSessionWithPermissionHandler();
}
if (session == null) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Sub-agent tool calls bypass SDK session hooks and permission handlers (was: onPreToolUse hook does not fire for sub-agent tool calls)

3 participants