Debugging a Copilot CLI Extension
When One Line Breaks Everything
The problem: A Copilot CLI extension, to generate kid storybooks, was running all fine — when suddenly it completely broke — it wouldn’t load, and when it did, every tool call silently failed.
Root cause: CLI version 1.0.40 dropped support for --stdio as a standalone flag, so the extension’s internal helper session would immediately exit with error: Invalid command format. The fix turned out to be a single missing argument in a constructor call. Here’s how I got there.
The Setup
I recently created a kid-story-creator extension in .github/extensions and hooked into Copilot CLI to interactively generate kids’ storybooks. It worked by spinning up a second Copilot session internally to do the heavy lifting of story generation via CopilotClient from @github/copilot-sdk.
With a recent CLI update & a second session is where everything broke.
Phase 1: The Extension Wouldn’t Even Load
The first sign of trouble: extensions reload came back with kid-story-creator simply absent — no error in the UI, just missing. Running the file directly with node surfaced the first problem immediately:
SyntaxError: Unexpected token ‘}’ at line 392The updateCatalog function body was floating without its function signature — a broken edit had removed it. I restored the file from a known-good git commit and the extension came back as ready
Phase 2: Ready, But Silently Failing
With the extension loading cleanly, running kid_story_create returned Tool execution failed — no details, no stack trace, just silence. The extension was alive but story generation was dead.
The breakthrough came from reading the CLI logs in ~/.copilot/logs/:
[INFO] Extension ready: .../kid-story-creator/extension.mjs
[INFO] [CLI subprocess] error: Invalid command format.
[INFO] [CLI subprocess] Did you mean:
copilot -i “...index.js --headless --no-auto-update --log-level debug --stdio”?
[WARNING] External tool call failed: CLI server exited with code 1This pointed directly at the nested CopilotClient call. The SDK was trying to spawn a new CLI process for the helper session — and failing.
What’s actually happening: CLI version 1.0.40 no longer accepts --stdio as a standalone flag. It expects copilot -i (interactive) or copilot -p "<prompt>" (non-interactive) format. So the spawned helper process immediately exits with error: Invalid command format — and every tool call that depends on that session fails silently.
Phase 3: The One-Line Fix
Digging into SDK source revealed the exact mechanism. CopilotClient() with no arguments resolves the CLI path to index.js, then prepends process.execPath as the Node runner to execute it. But inside an extension subprocess, process.execPath is the Copilot binary itself — not Node. So the SDK was effectively spawning:
copilot index.js --headless --no-auto-update --log-level debug --stdio
↑ ↑
binary treated as unknown subcommand → “Invalid command format”The fix: pass cliPath: process.execPath explicitly. Since the binary path doesn’t end in .js, SDK skips the Node-runner wrapping and spawns the binary directly — which correctly handles --headless as a recognised flag.
// Before — SDK auto-resolves cliPath to index.js, then wraps with process.execPath
const client = new CopilotClient();// After — SDK spawns the binary directly: copilot --headless --stdio ✓
const client = new CopilotClient({ cliPath: process.execPath });One property. One line. Extension reloaded. Storybook generated.
What to Take Away
If you’re building Copilot CLI extensions that spin up nested CopilotClient sessions, keep this in mind, and ask your copilot to store this info:
process.execPath inside an extension is the Copilot binary, not Node. The SDK assumes it can use process.execPath as a Node runner — but in an extension subprocess, that assumption breaks.
Always pass cliPath: process.execPath when creating a nested CopilotClient. This bypasses the .js detection branch and lets the binary handle its own startup flags correctly.
The CLI logs are your best friend. The error was completely invisible in the UI — only ~/.copilot/logs/ revealed the actual subprocess failure message that led to the fix.
Read the SDK source. The logic in startCLIServer() is readable and short — understanding the .js vs binary branch made the fix obvious in seconds.
Whole investigation took longer than the fix — almost always how it goes.
Curious about what kid-story-creator extension actually does once it’s running? From age-appropriate story arcs to per-page illustration prompts and a morals library — there’s a lot hidden under the hood. Stay tuned for a deep dive!



