-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Describe the bug
Written by AI
Multiple extensions registering onUserPromptSubmitted (or any hook type) silently break each other. The CLI server calls updateOptions({ hooks: ... }) for each extension during initializeSession, which replaces the previous extension's hooks instead of merging them. Only the last extension to load gets working hooks — all others are silently dropped.
This means any user with 2+ hook-bearing extensions will have non-deterministic behavior depending on extension load order, with no error or warning logged.
Related: #2142 (another hooks bug in the same code area, assigned to @MRayermannMSFT)
Affected version
1.0.10
Steps to reproduce the behavior
- Create two extensions with
onUserPromptSubmitted:
Extension A (~/.copilot/extensions/ext-a/extension.mjs):
import { joinSession } from "@github/copilot-sdk/extension";
const session = await joinSession({
hooks: {
onUserPromptSubmitted: async (input) => {
await session.log("Hook A fired");
},
},
tools: [],
});Extension B (~/.copilot/extensions/ext-b/extension.mjs):
import { joinSession } from "@github/copilot-sdk/extension";
const session = await joinSession({
hooks: {
onUserPromptSubmitted: async (input) => {
await session.log("Hook B fired");
},
},
tools: [],
});- Start a new session (or
/clearto reload extensions). - Type any message.
- Check the timeline — only one of "Hook A fired" or "Hook B fired" appears (whichever extension loaded last).
Expected behavior
Both "Hook A fired" and "Hook B fired" should appear in the timeline. Extension hooks should be merged (array-concatenated per hook type), not replaced.
Additional context
Log evidence
With --log-level all, the logs show each extension sends a separate session.resume request with hooks: true:
08:06:11.984Z [DEBUG] Received session.resume request: {"sessionId":"...","tools":[{"name":"cwd_auto_record",...}],"hooks":true,...}
08:06:11.985Z [DEBUG] Received session.resume request: {"sessionId":"...","tools":[],"hooks":true,...}
08:06:11.985Z [DEBUG] Received session.resume request: {"sessionId":"...","tools":[],"hooks":true,...}
08:06:11.985Z [DEBUG] Received session.resume request: {"sessionId":"...","tools":[],"hooks":true,...}
08:06:11.990Z [DEBUG] Received session.resume request: {"sessionId":"...","tools":[{"name":"open_session_viewer",...}],...}
But only one userPromptSubmitted dispatch occurs per user message:
08:06:38.547Z [DEBUG] Dispatching userPromptSubmitted hook for session dcdc7923-...
And previous extension connections fail silently:
08:06:25.386Z [ERROR] Hook preToolUse failed for session dcdc7923-...: Error: Connection is disposed.
This shows the server processes multiple session.resume requests with hooks: true, but each one overwrites the hooks from the previous extension instead of merging them. The result is that only the last extension's hooks proxy survives.
Impact
- Any user with 2+ hook-bearing extensions has non-deterministic behavior depending on extension load order
onUserPromptSubmittedhooks used for slash command interception oradditionalContextinjection silently break- The
Connection is disposederror suggests stale hook proxies may also cause errors for other hook types
Suggested fix
When processing session.resume requests with hooks: true, merge the new extension's hook arrays with existing hooks instead of replacing them. The codebase already has a hook-merging function (used for JSON config hooks) that concatenates arrays per hook type.
Workaround
Only one extension should register hooks. Other extensions needing slash command behavior should use tools with skipPermission: true instead.