Skip to content

Extensions with joinSession hooks override/disable hooks.json hooks #2076

Closed

Description

@htekdev

Describe the bug

When a CLI extension registers hooks via joinSession (e.g., onPreToolUse, onPostToolUse), the hooks previously defined in hooks.json stop firing. The extension hooks appear to replace/override the hooks.json pipeline rather than composing with it.

Expected: Extension hooks and hooks.json hooks should both run — extensions should layer on top of existing hooks, not replace them.

Actual: Once an extension with joinSession hooks is loaded, the hooks defined in hooks.json are no longer honored.

Affected version

GitHub Copilot CLI 1.0.5

Steps to reproduce the behavior

  1. Create a hooks.json (or .github/hooks/hooks.json) with a hook — e.g., an onPreToolUse hook that logs or blocks a specific command.
  2. Verify the hook fires as expected (it should).
  3. Create a CLI extension in .github/extensions/ that calls joinSession with its own hooks (e.g., onPreToolUse or onPostToolUse).
  4. Start a new Copilot CLI session so the extension loads.
  5. Trigger the same action that would fire the hooks.json hook.
  6. Observe: The hooks.json hook no longer fires. Only the extension's hooks run.

Expected behavior

Both the hooks.json hooks and the extension hooks should fire. Extensions should compose with existing hooks rather than replacing them. The hook pipeline should be additive — an extension registering onPreToolUse should not prevent a hooks.json onPreToolUse from also executing.

Additional context

  • This is consistently reproducible.
  • Reported via the GitHub Copilot at Microsoft Teams channel.
  • Related (but different): Custom agents with builtin agent names are silently ignored #1528 covers custom agents with builtin names being silently ignored — this issue is specifically about extension hooks overriding hooks.json hooks.

Activity

  1. shsolomo commented on Mar 19, 2026

    @shsolomo

    Root Cause Analysis

    I hit this same bug and traced through the CLI source (v1.0.8). Here's the exact code path:

    The Overwrite

    In initializeSession(), each extension's hooks are registered via updateOptions:

    n.updateOptions({ hooks: this.createHooksProxy(I, r) })

    The base updateOptions does direct assignment:

    this.hooks = e.hooks  // replacement, not merge

    So the last extension to call updateOptions wins all previously registered extension hooks are silently lost.

    Why It Only Affects Extensions (Not hooks.json)

    A merge function Uxt() exists in the codebase and is used to compose hooks from hooks.json config files. But this merge is not applied when extensions register hooks via updateOptions. The fix should apply the same merge logic to extension hooks.

    The Proxy Architecture

    createHooksProxy(I, r) returns per-connection hook arrays like:

    { sessionStart: [o => r("sessionStart", o)] }

    where r sends a hooks.invoke RPC to that specific extension's connection. When overwritten by the next extension's proxy, those RPC callbacks are lost the extension never receives hook invocations.

    Workaround

    session.on("user.message") + session.send() event listeners are registered per-connection and are not subject to the overwrite. This is what I'm using in my extension now.

    Related

    I filed #2142 for a related but separate issue: onSessionStart's additionalContext return value is also silently ignored (fire-and-forget invocation with no result capture).

  2. MRayermannMSFT commented on Mar 23, 2026

    @MRayermannMSFT

    Fix gone out in latest CLI pre-release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions