Skip to content

Extension hooks replaced instead of merged #2210

@Arithmomaniac

Description

@Arithmomaniac

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

  1. 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: [],
});
  1. Start a new session (or /clear to reload extensions).
  2. Type any message.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions