Skip to content

Document upstream SDK behavior for sub-agent hook/permission dispatch#68

Open
Copilot wants to merge 4 commits intomainfrom
copilot/fix-onpretooluse-for-subagents
Open

Document upstream SDK behavior for sub-agent hook/permission dispatch#68
Copilot wants to merge 4 commits intomainfrom
copilot/fix-onpretooluse-for-subagents

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 11, 2026

Resolves #125


Before the change?

  • hooks.invoke and permission.request RPC handlers look up sessions strictly by ID. Sub-agent sessions are created internally by the CLI with their own session IDs that are never registered in the SDK's session map. This causes handleHooksInvoke to return a -32602 error and handlePermissionRequest to return denied, silently swallowing the hook/permission call for any tool the sub-agent invokes.

After the change?

  • After investigating all three upstream SDKs (.NET, Node.js, Go), all of them handle hooks.invoke and permission.request identically — they throw/error for unknown session IDs with no fallback logic:

    • .NET: client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}")
    • Node.js: throw new Error("Session not found: " + params.sessionId)
    • Go: return nil, &jsonrpc2.Error{Code: -32602, Message: "unknown session " + req.SessionID}
  • An initial SDK-level fallback approach was implemented and then reverted to match upstream behavior. The sub-agent hooks gap exists across all SDK implementations and should be addressed at the CLI/protocol level (the CLI should route sub-agent hooks using the parent session ID) rather than with SDK-side heuristics.

  • No code changes remain — the Java SDK behavior matches the upstream .NET/Node.js/Go SDKs.

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 and others added 2 commits April 11, 2026 23:29
When the CLI creates sub-agent sessions internally, their session IDs
are not in the SDK's session registry. The hooks.invoke and
permission.request handlers now fall back to any registered session
that has the appropriate handler, enabling hooks and permission checks
to fire for sub-agent tool calls.

Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/868c6fed-c57d-4d9f-806c-eca509096672

Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix onPreToolUse hook for sub-agent tool calls Fix onPreToolUse hook not firing for sub-agent tool calls Apr 11, 2026
Copilot AI requested a review from brunoborges April 11, 2026 23:32
@brunoborges brunoborges marked this pull request as ready for review April 12, 2026 00:22
Copilot AI review requested due to automatic review settings April 12, 2026 00:22
@brunoborges
Copy link
Copy Markdown
Collaborator

@copilot shouldn't subagents ask for the parent session for hooks and permissions? How is .NET SDK doing this?

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.

Pull request overview

This PR fixes SDK-side dispatching of hooks.invoke and permission.request for CLI-created sub-agent sessions whose session IDs are not present in the SDK session registry, ensuring tool hooks and permission handlers can still run.

Changes:

  • Add internal CopilotSession query methods to detect whether hooks / permission handlers are registered.
  • Update RpcHandlerDispatcher to fall back to a registered session with hooks/permission handlers when the incoming sessionId is unknown (and make hooks.invoke a no-op with output: null if none exist).
  • Add/adjust unit tests to cover fallback and no-fallback behavior for both hooks and permission requests.
Show a summary per file
File Description
src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java Adds fallback session selection for unknown sessionId in permission.request and hooks.invoke, plus helper finders.
src/main/java/com/github/copilot/sdk/CopilotSession.java Adds package-private hasHooks() / hasPermissionHandler() helpers used by dispatcher fallback.
src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java Adds tests for fallback/no-op behavior for sub-agent permission and hook invocations.

Copilot's findings

Comments suppressed due to low confidence (3)

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

  • When falling back for an unknown sessionId, the permission handler is invoked via the chosen fallback CopilotSession, but the PermissionInvocation inside CopilotSession uses that session’s own sessionId. This means sub-agent permission requests will present the parent (fallback) sessionId to the handler, losing the actual request sessionId from the RPC params. Consider plumbing the original sessionId through (e.g., pass it into handlePermissionRequest / PermissionInvocation) so handlers can correctly attribute sub-agent requests.

This issue also appears on line 296 of the same file.

                String sessionId = params.get("sessionId").asText();
                JsonNode permissionRequest = params.get("permissionRequest");

                CopilotSession session = sessions.get(sessionId);
                if (session == null) {
                    // The CLI may send permission requests for sub-agent sessions
                    // whose IDs are not tracked by the SDK. Fall back to any
                    // registered session that has a permission handler.
                    session = findSessionWithPermissionHandler();
                }
                if (session == null) {
                    var result = new PermissionRequestResult()
                            .setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER);
                    rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
                    return;
                }

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

  • When falling back for an unknown sessionId, the HookInvocation created in CopilotSession uses the fallback session’s sessionId, not the RPC param sessionId. For sub-agent hooks.invoke calls this can misattribute which session triggered onPreToolUse/onPostToolUse. Consider passing the original sessionId into hook handling so HookInvocation reflects the request’s sessionId even when a different session instance provides the handlers.
                String sessionId = params.get("sessionId").asText();
                String hookType = params.get("hookType").asText();
                JsonNode input = params.get("input");

                CopilotSession session = sessions.get(sessionId);
                if (session == null) {
                    // The CLI may send hooks.invoke for sub-agent sessions whose IDs
                    // are not tracked by the SDK. Fall back to any registered session
                    // that has hooks so that application-level hooks (e.g. onPreToolUse)
                    // also fire for sub-agent tool calls.
                    session = findSessionWithHooks();
                }
                if (session == null) {
                    rpc.sendResponse(Long.parseLong(requestId), Collections.singletonMap("output", null));
                    return;
                }

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

  • findSessionWithPermissionHandler() has the same non-deterministic “first match” issue as findSessionWithHooks(): if more than one session has a permission handler, ConcurrentHashMap iteration order makes the fallback session unpredictable, which can change allow/deny decisions unexpectedly. Consider requiring a single unambiguous fallback candidate or adding a deterministic selection mechanism.
     * If multiple sessions have permission handlers, the first one found is
     * returned. In practice, SDK users typically register a single permission
     * handler that covers all sub-agent activity.
     *
     * @return a session with a permission handler, or {@code null} if none found
     */
    private CopilotSession findSessionWithPermissionHandler() {
        for (CopilotSession s : sessions.values()) {
            if (s.hasPermissionHandler()) {
                return s;
            }
        }
        return null;
    }
  • Files reviewed: 3/3 changed files
  • Comments generated: 1

Comment on lines +341 to +354
* If multiple sessions have hooks registered, the first one found is returned.
* In practice, SDK users typically register hooks on a single session that
* covers all sub-agent activity.
*
* @return a session with hooks, or {@code null} if none found
*/
private CopilotSession findSessionWithHooks() {
for (CopilotSession s : sessions.values()) {
if (s.hasHooks()) {
return s;
}
}
return null;
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

findSessionWithHooks() returns the “first” session found with hooks, but sessions is a ConcurrentHashMap so iteration order is not deterministic. If multiple sessions register hooks, fallback selection becomes effectively random and could run the wrong hooks. Consider only falling back when exactly one candidate session has hooks (otherwise return an error / null), or introduce a deterministic selection strategy (e.g., track a designated ‘default hooks’ session).

This issue also appears on line 362 of the same file.

Copilot uses AI. Check for mistakes.
All three upstream SDKs (C#, TypeScript, Go) throw/error for unknown
session IDs in hooks.invoke and permission.request handlers with no
fallback logic. The sub-agent hooks gap exists in all SDK
implementations and should be addressed at the CLI/protocol level
rather than with SDK heuristics.

Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/50bf8907-712c-4792-aeea-219889b1ffa9

Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 12, 2026

@copilot shouldn't subagents ask for the parent session for hooks and permissions? How is .NET SDK doing this?

I checked all three upstream SDKs. They all throw/error for unknown session IDs in hooks.invoke and permission.request with no fallback logic:

  • .NET: client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}")
  • Node.js: throw new Error("Session not found: " + params.sessionId)
  • Go: return nil, &jsonrpc2.Error{Code: -32602, Message: "unknown session " + req.SessionID}

None of the upstream SDKs implement sub-agent hook forwarding. The gap exists across all SDK implementations, meaning the issue should be addressed at the CLI/protocol level (the CLI should route sub-agent hooks using the parent session ID).

Reverted the SDK-level fallback in 1d0bf62 to match upstream behavior.

Copilot AI changed the title Fix onPreToolUse hook not firing for sub-agent tool calls Document upstream SDK behavior for sub-agent hook/permission dispatch Apr 12, 2026
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]: onPreToolUse hook does not fire for sub-agent tool calls

3 participants