- Home
- Documentation
- extensions
- Extension Loading (TypeScript/JavaScript Modules)
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).
What this subsystem does
Section titled “What this subsystem does”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
Primary implementation files
Section titled “Primary implementation files”src/extensibility/extensions/loader.ts— path discovery + import/executionsrc/extensibility/extensions/index.ts— public exportssrc/extensibility/extensions/runner.ts— runtime/event execution after loadsrc/discovery/builtin.ts— native auto-discovery provider for extension modulessrc/config/settings.ts— loads mergedextensions/disabledExtensionssettings
Inputs to extension loading
Section titled “Inputs to extension loading”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
.xcshbased. - Legacy
.piis still accepted inpackage.jsonmanifest keys (pi.extensions), but not as a native root here.
2) Explicitly configured paths
Section titled “2) Explicitly configured paths”After auto-discovery, configured paths are appended and resolved.
Configured path sources in the main session startup path (sdk.ts):
- CLI-provided paths (
--extension/-e, and--hookis also treated as an extension path) - Settings
extensionsarray (merged global + project settings)
Global settings file:
~/.xcsh/agent/config.yml(or custom agent dir viaPI_CODING_AGENT_DIR)
Project settings file:
<cwd>/.xcsh/settings.json
Examples:
extensions: - ~/my-exts/safety.ts - ./local/ext-pack{ "extensions": ["./.xcsh/extensions/my-extra"]}Enable/disable controls
Section titled “Enable/disable controls”Disable discovery
Section titled “Disable discovery”- CLI:
--no-extensions - SDK option:
disableExtensionDiscovery
Behavior split:
- SDK: when
disableExtensionDiscovery=true, it still loadsadditionalExtensionPathsvialoadExtensions(). - CLI path building (
main.ts) currently clears CLI extension paths when--no-extensionsis set, so explicit-e/--hookare not forwarded in that mode.
Disable specific extension modules
Section titled “Disable specific extension modules”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:fooPath and entry resolution
Section titled “Path and entry resolution”Path normalization
Section titled “Path normalization”For configured paths:
- Normalize unicode spaces
- Expand
~ - If relative, resolve against current
cwd
If configured path is a file
Section titled “If configured path is a file”It is used directly as a module entry candidate.
If configured path is a directory
Section titled “If configured path is a directory”Resolution order:
package.jsonin that directory withxcsh.extensions(or legacypi.extensions) -> use declared entriesindex.tsindex.js- Otherwise scan one level for extension entries:
- direct
*.ts/*.js - subdir
index.ts/index.js - subdir
package.jsonwithxcsh.extensions/pi.extensions
- direct
Rules and constraints:
- no recursive discovery beyond one subdirectory level
- declared
extensionsmanifest 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
Ignore behavior differs by source
Section titled “Ignore behavior differs by source”- Native auto-discovery (
discoverExtensionModulePathsin discovery helpers) uses native glob withgitignore: trueandhidden: false. - Explicit configured directory scanning in
loader.tsusesreaddirrules and does not apply gitignore filtering.
Load order and precedence
Section titled “Load order and precedence”discoverAndLoadExtensions() builds one ordered list and then calls loadExtensions().
Order:
- Native auto-discovered modules
- Explicit configured paths (in provided order)
In sdk.ts, configured order is:
- CLI additional paths
- 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).
Module import and factory contract
Section titled “Module import and factory contract”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.
Failure handling and isolation
Section titled “Failure handling and isolation”During loading
Section titled “During loading”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
Runtime isolation model
Section titled “Runtime isolation model”- Extensions are not sandboxed (same process/runtime).
- They share one
EventBusand oneExtensionRuntimeinstance. - During load, runtime action methods intentionally throw
ExtensionRuntimeNotInitializedError; action wiring happens later inExtensionRunner.initialize().
After loading
Section titled “After loading”When events run through ExtensionRunner, handler exceptions are caught and emitted as extension errors instead of crashing the runner loop.
Minimal user/project layout examples
Section titled “Minimal user/project layout examples”User-level
Section titled “User-level”~/.xcsh/agent/ config.yml extensions/ guardrails.ts audit/ index.tsProject-level
Section titled “Project-level”<repo>/ .xcsh/ settings.json extensions/ checks/ package.json lint-gates.tschecks/package.json:
{ "xcsh": { "extensions": ["./src/check-a.ts", "./src/check-b.js"] }}Legacy manifest key still accepted:
{ "pi": { "extensions": ["./index.ts"] }}