Repository navigation
Extensions fail to load — SEA cache directory path mismatch (universal/ vs darwin-arm64/) #2890
Description
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
-
Install copilot (either method):
npm install -g @github/copilot # or: brew install --cask copilot-cli -
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-
Create a minimal project extension:
mkdir -p /tmp/copilot-ext-repro/.github/extensions/hello-ext cd /tmp/copilot-ext-repro git initCreate
.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");
-
Run copilot:
copilot -p "say hello" -
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.
Activity
polajenko commentedon Apr 24, 2026
Windows (npm install) confirmation + proposed fix
Confirming this is also broken on Windows when installed via npm. Same security check, different path topology.
Environment:
npm install -g @github/copilotcopilot-win32-x64)Path mismatch on Windows (npm install):
The SEA binary lives inside the npm package:
There is no
preloads/directory in the SEA binary's package — it only containscopilot.exe,package.json,LICENSE.md, andREADME.md.The
preloads/directory only exists in the auto-update pkg cache:So when
app.js(loaded fromuniversal/1.0.34/) callsfork()with the bootstrap path fromuniversal/, the child process's security check resolvesZto the SEA binary directory and rejects the bootstrap path:Log evidence:
Both user-level (
~/.copilot/extensions/xlsx/) and project-level (.github/extensions/aegis/) extensions fail identically.Key difference from macOS report: On macOS, the mismatch is between
darwin-arm64/anduniversal/subdirs in the same cache root. On Windows with npm install, the mismatch is between the npmnode_modulesdirectory (where the SEA binary lives) and the~/.copilot/pkg/universal/cache (where auto-updated assets live). Thepreloads/directory never exists alongside the SEA binary, so no workaround is possible short of patchingindex.js.The macOS workaround (deleting
universal/) does not apply here because on Windows the SEA binary's own directory (copilot-win32-x64/) never containspreloads/.Proposed fix
The root cause is that
launchExtension()builds the bootstrap path fromcliDistDir(the pkg cache), but the forked child's security check inindex.jsexpects it underdirname(process.execPath)(the SEA binary dir).Option A — Fix in
launchExtension()(smallest change):In the unminified source,
launchExtensioncurrently does something like:Change to resolve the bootstrap relative to the SEA binary, since that's what the child process validates against:
This requires shipping
preloads/alongside the platform binary in the npm package (i.e., addingextension_bootstrap.mjsandextension_sdk_resolver.mjsto@github/copilot-win32-x64,@github/copilot-darwin-arm64, etc.).Option B — Fix in
index.jssecurity check (more robust):Instead of only trusting
dirname(import.meta.url) + "/preloads/", also trust thecliDistDirfrom the pkg cache. For example, accept bootstrap paths from any trusted~/.copilot/pkg/subdirectory, or pass the trusted directory as an env var from the parent:Option B is more robust because it doesn't require shipping preloads in every platform package, but Option A is simpler and doesn't change the security boundary.
I verified Option A works locally by copying
preloads/to the SEA dir and patching the minifiedapp.jsto preferdirname(process.execPath). Both user-level and project-level extensions load successfully after the fix.copilot-cli-bot commentedon Sep 16, 2026
This appears to be resolved as of GitHub Copilot CLI v1.0.85. Please update to that release or a newer stable version. We're closing this as completed. If it still happens, please comment with your version and reproduction steps.