Skip to content

Extensions fail to load — SEA cache directory path mismatch (universal/ vs darwin-arm64/) #2890

Description

@pbleisch

Describe the bug

Project extensions (.github/extensions/) fail to load when both universal/
and platform-specific (e.g. darwin-arm64/) cache directories exist under
~/Library/Caches/copilot/pkg/. The extension subprocess immediately exits with:

error: Invalid command format.
Did you mean: copilot -i "<path>/extension_bootstrap.mjs"?

What I think is happening

From reading the minified index.js in the cache, it looks like the SEA
binary's import.meta.url resolves to the platform-specific cache path
(darwin-arm64/<ver>/). I'll call this Z (the variable name in the minified
code).

There's a security check early in index.js that validates extension bootstrap
paths against Z:

var Z = dirname(fileURLToPath(import.meta.url));  // → darwin-arm64/<ver>/
var K = process.argv.find(e => basename(e) === "extension_bootstrap.mjs");
var Q = K ? resolve(K) : undefined;
var Y = Q?.startsWith(resolve(Z, "preloads") + sep) ? Q : undefined;

But the bootstrap path that app.js passes to fork() comes from universal/,
not darwin-arm64/. So the prefix check fails and Y ends up undefined.

It appears that when COPILOT_RUN_APP=1 is set (which it is, inherited from
the parent process), the code that loads app.js searches only universal/
subdirectories:

let dirs = D().map(i => join(i, "universal"));
let found = await M("app.js", ...dirs);

Once app.js loads from universal/<ver>/, its own import.meta.url becomes
universal/<ver>/app.js, so cliDistDir = universal/<ver>/. When it forks
extensions, the bootstrap path is universal/<ver>/preloads/extension_bootstrap.mjs.
The forked SEA child then rejects this because Z is still darwin-arm64/<ver>/.

I also noticed that COPILOT_RUN_APP=1 appears to leak into the forked
extension environment — the env proxy in the fork call doesn't seem to block it.

Affected version

GitHub Copilot CLI 1.0.34

Steps to reproduce the behavior

  1. Install copilot (either method):

    npm install -g @github/copilot
    # or: brew install --cask copilot-cli
  2. Ensure both cache directories exist under ~/Library/Caches/copilot/pkg/:

    ls ~/Library/Caches/copilot/pkg/
    # Should show both: darwin-arm64/  universal/

How these directories get created: The SEA binary extracts its embedded JS assets to darwin-arm64/<version>/ (using the platform and architecture in the path). Separately, the auto-update mechanism in app.js downloads and writes new versions to universal/<version>/.

On my machine, darwin-arm64/1.0.34/ was created on Apr 20 at 19:54 (likely when the SEA first ran after an update), and universal/1.0.34/ appeared on Apr 21 at 07:59 (likely from the auto-update check). Both directories contain identical file contents — the path difference is the only thing that matters.

If universal/ doesn't exist yet, you can trigger it by running copilot a few times or waiting for an auto-update cycle. Or copy it manually:

cp -r ~/Library/Caches/copilot/pkg/darwin-arm64/1.0.34 \
      ~/Library/Caches/copilot/pkg/universal/1.0.34
  1. Create a minimal project extension:

    mkdir -p /tmp/copilot-ext-repro/.github/extensions/hello-ext
    cd /tmp/copilot-ext-repro
    git init

    Create .github/extensions/hello-ext/extension.mjs:

    import { joinSession } from "@github/copilot-sdk";
    const session = await joinSession();
    session.onPreToolUse("*", (event) => {
      console.error("[hello-ext] tool called:", event.toolName);
      return { proceed: true };
    });
    console.error("[hello-ext] Extension loaded successfully");
  2. Run copilot:

    copilot -p "say hello"
  3. Check the process log in ~/.copilot/logs/:

    [INFO] Launching extension: .github/extensions/hello-ext/extension.mjs
    [INFO] [extension:...] error: Invalid command format.
    [INFO] [extension:...] Did you mean: copilot -i ".../universal/.../extension_bootstrap.mjs"?
    [ERROR] Extension failed during startup: ... (code=1, signal=null)
    

Expected behavior

The extension should load and run. [hello-ext] Extension loaded successfully
should appear in stderr, and the onPreToolUse hook should fire when tools are
called.

Additional context

  • OS: macOS (Darwin arm64)
  • Shell: zsh
  • Node.js (embedded in SEA): v24.11.1

What I observed to confirm this

I ran the SEA binary directly with each bootstrap path:

SEA=<path-to-sea-binary>

# Bootstrap from universal/ (what app.js currently does) → FAILS
$ COPILOT_RUN_APP=1 $SEA ~/Library/Caches/copilot/pkg/universal/1.0.34/preloads/extension_bootstrap.mjs
# error: Invalid command format.

# Bootstrap from darwin-arm64/ (matching where Z points) → WORKS
$ COPILOT_RUN_APP=1 $SEA ~/Library/Caches/copilot/pkg/darwin-arm64/1.0.34/preloads/extension_bootstrap.mjs
# [extension-bootstrap] No extension path provided  ← bootstrap loaded!

I also placed a test script in darwin-arm64/1.0.34/preloads/ that exits with
code 42. Running it through the SEA binary confirmed the security check passes
for paths under darwin-arm64/. The same script placed in universal/preloads/
was rejected.

Workaround

Deleting the universal/ cache forces app.js to load from darwin-arm64/
(matching Z), which makes the paths align:

rm -rf ~/Library/Caches/copilot/pkg/universal/

Not persistent — the universal/ directory may be recreated by subsequent
copilot runs or auto-updates.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:installationInstalling, updating, versioning, PATH setup, and binary distributionarea:pluginsPlugin system, marketplace, hooks, skills, extensions, and custom agents

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions