Skip to content

Extension Loading (TypeScript/JavaScript Modules)

Extension Loading (TypeScript/JavaScript Modules)

Section titled “Extension Loading (TypeScript/JavaScript Modules)”

This document covers how the coding agent discovers and loads extension modules (.ts/.js) at startup.

It does not cover gemini-extension.json manifest extensions (documented separately).

Extension loading builds a list of module entry files, imports each module with Bun, executes its factory, and returns:

  • loaded extension definitions
  • per-path load errors (without aborting the whole load)
  • a shared extension runtime object used later by ExtensionRunner
  • src/extensibility/extensions/loader.ts — path discovery + import/execution
  • src/extensibility/extensions/index.ts — public exports
  • src/extensibility/extensions/runner.ts — runtime/event execution after load
  • src/discovery/builtin.ts — native auto-discovery provider for extension modules
  • src/config/settings.ts — loads merged extensions / disabledExtensions settings

1) Auto-discovered native extension modules

Section titled “1) Auto-discovered native extension modules”

discoverAndLoadExtensions() first asks discovery providers for extension-module capability items, then keeps only provider native items.

Effective native locations:

  • Project: <cwd>/.xcsh/extensions
  • User: ~/.xcsh/agent/extensions

Path roots come from the native provider (SOURCE_PATHS.native).

Notes:

  • Native auto-discovery is currently .xcsh based.
  • Legacy .pi is still accepted in package.json manifest keys (pi.extensions), but not as a native root here.

After auto-discovery, configured paths are appended and resolved.

Configured path sources in the main session startup path (sdk.ts):

  1. CLI-provided paths (--extension/-e, and --hook is also treated as an extension path)
  2. Settings extensions array (merged global + project settings)

Global settings file:

  • ~/.xcsh/agent/config.yml (or custom agent dir via PI_CODING_AGENT_DIR)

Project settings file:

  • <cwd>/.xcsh/settings.json

Examples:

~/.xcsh/agent/config.yml
extensions:
- ~/my-exts/safety.ts
- ./local/ext-pack
{
"extensions": ["./.xcsh/extensions/my-extra"]
}

  • CLI: --no-extensions
  • SDK option: disableExtensionDiscovery

Behavior split:

  • SDK: when disableExtensionDiscovery=true, it still loads additionalExtensionPaths via loadExtensions().
  • CLI path building (main.ts) currently clears CLI extension paths when --no-extensions is set, so explicit -e/--hook are not forwarded in that mode.

disabledExtensions setting filters by extension id format:

  • extension-module:<derivedName>

derivedName is based on entry path (getExtensionNameFromPath), for example:

  • /x/foo.ts -> foo
  • /x/bar/index.ts -> bar

Example:

disabledExtensions:
- extension-module:foo

For configured paths:

  1. Normalize unicode spaces
  2. Expand ~
  3. If relative, resolve against current cwd

It is used directly as a module entry candidate.

Resolution order:

  1. package.json in that directory with xcsh.extensions (or legacy pi.extensions) -> use declared entries
  2. index.ts
  3. index.js
  4. Otherwise scan one level for extension entries:
    • direct *.ts / *.js
    • subdir index.ts / index.js
    • subdir package.json with xcsh.extensions / pi.extensions

Rules and constraints:

  • no recursive discovery beyond one subdirectory level
  • declared extensions manifest entries are resolved relative to that package directory
  • declared entries are included only if file exists/access is allowed
  • in */index.{ts,js} pairs, TypeScript is preferred over JavaScript
  • symlinks are treated as eligible files/directories
  • Native auto-discovery (discoverExtensionModulePaths in discovery helpers) uses native glob with gitignore: true and hidden: false.
  • Explicit configured directory scanning in loader.ts uses readdir rules and does not apply gitignore filtering.

discoverAndLoadExtensions() builds one ordered list and then calls loadExtensions().

Order:

  1. Native auto-discovered modules
  2. Explicit configured paths (in provided order)

In sdk.ts, configured order is:

  1. CLI additional paths
  2. Settings extensions

De-duplication:

  • absolute path based
  • first seen path wins
  • later duplicates are ignored

Implication: if the same module path is both auto-discovered and explicitly configured, it is loaded once at the first position (auto-discovered stage).


Each candidate path is loaded with dynamic import:

  • await import(resolvedPath)
  • factory is module.default ?? module
  • factory must be a function (ExtensionFactory)

If export is not a function, that path fails with a structured error and loading continues.


Per extension path, failures are captured as { path, error } and do not stop other paths from loading.

Common cases:

  • import failure / missing file
  • invalid factory export (non-function)
  • exception thrown while executing factory
  • Extensions are not sandboxed (same process/runtime).
  • They share one EventBus and one ExtensionRuntime instance.
  • During load, runtime action methods intentionally throw ExtensionRuntimeNotInitializedError; action wiring happens later in ExtensionRunner.initialize().

When events run through ExtensionRunner, handler exceptions are caught and emitted as extension errors instead of crashing the runner loop.


~/.xcsh/agent/
config.yml
extensions/
guardrails.ts
audit/
index.ts
<repo>/
.xcsh/
settings.json
extensions/
checks/
package.json
lint-gates.ts

checks/package.json:

{
"xcsh": {
"extensions": ["./src/check-a.ts", "./src/check-b.js"]
}
}

Legacy manifest key still accepted:

{
"pi": {
"extensions": ["./index.ts"]
}
}