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
/clear to 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
onUserPromptSubmitted hooks used for slash command interception or additionalContext injection silently break
- The
Connection is disposed error 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.
Describe the bug
Written by AI
Multiple extensions registering
onUserPromptSubmitted(or any hook type) silently break each other. The CLI server callsupdateOptions({ hooks: ... })for each extension duringinitializeSession, 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
onUserPromptSubmitted:Extension A (
~/.copilot/extensions/ext-a/extension.mjs):Extension B (
~/.copilot/extensions/ext-b/extension.mjs):/clearto reload extensions).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 separatesession.resumerequest withhooks: true:But only one
userPromptSubmitteddispatch occurs per user message:And previous extension connections fail silently:
This shows the server processes multiple
session.resumerequests withhooks: 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
onUserPromptSubmittedhooks used for slash command interception oradditionalContextinjection silently breakConnection is disposederror suggests stale hook proxies may also cause errors for other hook typesSuggested fix
When processing
session.resumerequests withhooks: 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: trueinstead.