Skip to content

Hooks and Patches

Corgi intercepts Kagi's runtime behavior through monkey-patching and event interception. This page documents the hooking layer that plugins use to modify search results, settings, network requests, and UI behavior.

Current Coverage

The hook API described here is fully implemented and available to every plugin through the PluginAPI object. That said, most built-in plugins still hardcode their DOM manipulation directly in onStart rather than routing through hooks. This works fine today, but the plan is to migrate those plugins to proper hook calls over time so cleanup and lifecycle management become automatic. If you are writing a new plugin, prefer the hook API from the start.

Hooking Strategy

Kagi assigns all its core objects and functions to window as plain globals with no module system, no bundler, and no framework in the way. This makes hooking straightforward compared to systems like Discord, which requires Webpack module factory interception.

Because Corgi's MAIN world script runs at document_start (before Kagi's scripts execute), it has two techniques available:

  1. Pre-assignment traps use Object.defineProperty to intercept globals that Kagi assigns during initialization, such as window.client and window.sseCache.
  2. Post-assignment patches wrap functions on prototypes and globals that already exist or will exist once a trap fires.

Object.defineProperty Traps

Some globals are assigned by Kagi's scripts during execution, and Corgi intercepts those assignments before the rest of the page can see them:

typescript
let _client: Client | undefined

Object.defineProperty(window, 'client', {
  configurable: true,
  get() { return _client },
  set(value) {
    _client = value
    // Wrap Client prototype methods now that the instance exists
    patchClientPrototype(value)
  }
})

Trapped globals:

GlobalKagi AssignmentCorgi Action
window.clientnew Client() at script loadWrap .connect(), .onSocketMessage(), .reload()
window.sseCachenew SSECache() at script loadWrap .replayEvents(), .storeEvents()
window.metricnew Metric() at script loadOptional: intercept telemetry events
window.kagiSettingsSet from cookie dataIntercept reads/writes to override settings

Function Wrapping

For functions that already exist on window or on prototypes, Corgi wraps them with before/after hooks that let plugins observe or modify calls without replacing the originals outright:

typescript
function wrapFunction<T extends (...args: any[]) => any>(
  obj: any,
  key: string,
  wrapper: (original: T, ...args: Parameters<T>) => ReturnType<T>
) {
  const original = obj[key] as T
  obj[key] = function(this: any, ...args: Parameters<T>) {
    return wrapper.call(this, original.bind(this), ...args)
  }
  // Preserve function name for debugging
  Object.defineProperty(obj[key], 'name', { value: key })
}

Primary wrap targets:

TargetPurpose
Client.prototype.onSocketMessageIntercept all SSE messages before dispatch. Plugins can modify, suppress, or inject provider events.
Client.prototype.connectIntercept search connections. Plugins can modify query parameters or add connection hooks.
SSECache.prototype.replayEventsIntercept cached result replay with the same hooks as live results.
window.getKagiSettingOverride or extend Kagi settings. Return custom values for settings Kagi does not know about.
window.setKagiSettingIntercept setting changes. Trigger plugin reactions to preference changes.
window.updateThemeHook theme application. Corgi can suppress or redirect theme changes.
window.initPageHook page initialization. Plugins can run setup before search begins.

Event Interception

Kagi dispatches search results and UI state through CustomEvents on window, and Corgi wraps window.addEventListener to intercept registration for any provider:* event:

typescript
const originalAddEventListener = window.addEventListener.bind(window)

window.addEventListener = function(type: string, listener: EventListener, options?: any) {
  if (type.startsWith('provider:')) {
    // Wrap the listener to pass events through plugin pipeline
    const wrappedListener = (event: CustomEvent) => {
      const result = pluginPipeline.processEvent(type, event)
      if (result !== false) {
        listener(result.modifiedEvent ?? event)
      }
    }
    return originalAddEventListener(type, wrappedListener, options)
  }
  return originalAddEventListener(type, listener, options)
}

Provider events available for interception:

EventContentHookable
provider:searchMain search results HTMLModify result HTML before render
provider:wikipediaWikipedia card HTMLModify or suppress
provider:right-sidebarRight sidebar contentInject or modify
provider:related_searchesRelated searchesModify suggestions
provider:widget_*Widgets (weather, stocks, etc.)Modify or suppress
provider:search.infoPagination, share URLsAccess metadata
provider:domain_infoDomain ranking data (JSON)Modify ranking display
provider:errorError messagesCustom error handling
client:search_initiatedSearch startedPre-search hooks
client:socket_openConnection openedConnection hooks
client:socket-closedConnection closedPost-search hooks

Fetch Interception

Corgi wraps window.fetch so plugins can inspect and modify API requests before they leave the browser and responses before they reach Kagi's handlers:

typescript
const originalFetch = window.fetch.bind(window)

window.fetch = async function(input: RequestInfo, init?: RequestInit) {
  const url = typeof input === 'string' ? input : input.url
  
  // Let plugins modify request before sending
  const modified = pluginPipeline.processRequest(url, init)
  
  const response = await originalFetch(modified.input, modified.init)
  
  // Let plugins modify response
  return pluginPipeline.processResponse(url, response)
}

Key fetch targets:

EndpointMethodPurpose
/esr/user_rulesPOSTDomain ranking (block/lower/higher/pin)
/autosuggest?q=...GETSearch suggestions
/api/wikipedia?q=...GETWikipedia data
/settingsPOSTSave user settings
/bangsPOSTCustom bang management
/mother/context?...POSTQuick Answer streaming

DOM Mutation Observer

A MutationObserver on document.documentElement watches for DOM changes, catching stylesheet additions (<link> and <style> elements), theme class changes on <html> (Kagi adds theme_* classes), and dynamic content injection like search results, modals, and widgets. Plugins can register mutation handlers to react to specific changes without polling.

Plugin Patch Format

Plugins can also define patches as declarative objects, and the patching system applies them during the hooking phase:

typescript
{
  patches: [
    {
      target: 'Client.prototype.onSocketMessage',
      type: 'after',
      handler(original, event) {
        // Runs after the original method
        console.log('SSE message received:', event)
      }
    },
    {
      target: 'window.getKagiSetting',
      type: 'replace',
      handler(original, key) {
        if (key === 'theme') return 'theme_dark'
        return original(key)
      }
    }
  ]
}

Patch types:

  • before: Run handler before the original function. Can modify arguments.
  • after: Run handler after the original function. Receives the return value.
  • replace: Replace the original entirely. Receives original as first argument for delegation.

Not affiliated with Kagi Inc.