github/copilot-sdk · error
Setup must only be called once
Error message
Setup must only be called once
What it means
Setup() is the one-time initializer for the embedded CLI package: it validates the runtime configuration, stores it in a package-level variable, and flips the setupDone flag. The library deliberately panics if Setup is called a second time because re-initialization would silently replace an already-locked configuration used by Path() and install helpers. This is an intentional fail-fast invariant, not an unexpected condition.
Solutions
- Guard the call with sync.Once (var once sync.Once; once.Do(func(){ embeddedcli.Setup(cfg) })) so Setup runs exactly once per process.
- Move the single Setup call to main()/package init of the entrypoint and remove Setup calls from library internals.
- In tests, call Setup once in TestMain or a shared helper rather than per-test.
Example fix
// before
func newClient() *Client {
embeddedcli.Setup(cfg)
return &Client{}
}
// after
var setupOnce sync.Once
func initCLI() {
setupOnce.Do(func() { embeddedcli.Setup(cfg) })
}
func newClient() *Client {
initCLI()
return &Client{}
} Defensive patterns
Strategy: try-catch
Validate before calling
var cliOnce sync.Once
func ensureSetup() {
cliOnce.Do(func() { embeddedcli.Setup(cfg) })
} Type guard
func setupNotDone() bool {
return !embeddedcliInitialized // package-level flag you control
} Try / catch
func safeSetup(cfg Config) (ok bool) {
defer func() {
if r := recover(); r != nil {
if strings.Contains(fmt.Sprint(r), "Setup must only be called once") {
ok = true // already initialized; treat as no-op
return
}
panic(r)
}
}()
embeddedcli.Setup(cfg)
return true
} Prevention
- Wrap Setup in sync.Once at the application entrypoint and never call it elsewhere.
- Expose a single internal initCLI() helper instead of letting libraries call Setup directly.
- In tests, initialize in TestMain, not per-test.
When it happens
Trigger: Calling embeddedcli.Setup(cfg) more than once in the same process — e.g. Setup called in package init of two packages, in both a library and its test harness, or re-invoked after an earlier Setup already set setupDone=true.
Common situations: Multiple packages' init() functions each calling Setup; test suites where several tests each call Setup instead of using a shared sync.Once; a library wrapper that defensively re-runs Setup on every client construction.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Setup must be called before Path is accessed
- Client not connected. Call start() first.
- failed to disconnect session
- Env is not supported with InProcessConnection: the…
- WorkingDirectory is not supported with InProcessConnection…
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/54e86058fd4f900a.
Report an issue: GitHub.
Appendix: source
Thrown at go/internal/embeddedcli/embeddedcli.go:85
panic("Cli reader is required")
}
if len(cfg.CliHash) != sha256.Size {
panic(fmt.Sprintf("CliHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.CliHash)))
}
if cfg.LinuxMuslCli != nil && len(cfg.LinuxMuslCliHash) != sha256.Size {
panic(fmt.Sprintf("LinuxMuslCliHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.LinuxMuslCliHash)))
}
if cfg.LinuxMuslRuntimeLib != nil && len(cfg.LinuxMuslRuntimeLibHash) != sha256.Size {
panic(fmt.Sprintf("LinuxMuslRuntimeLibHash must be a SHA-256 hash (%d bytes), got %d bytes", sha256.Size, len(cfg.LinuxMuslRuntimeLibHash)))
}
validateRuntimePairConfig(cfg.RuntimeExecutable, cfg.RuntimeExecutableHash, cfg.RuntimeNode, cfg.RuntimeNodeHash, "")
validateRuntimePairConfig(cfg.LinuxMuslRuntimeExecutable, cfg.LinuxMuslRuntimeExecutableHash, cfg.LinuxMuslRuntimeNode, cfg.LinuxMuslRuntimeNodeHash, "LinuxMusl")
validateOptionalHash(cfg.RuntimeAssets, cfg.RuntimeAssetsHash, "RuntimeAssetsHash")
validateOptionalHash(cfg.LinuxMuslRuntimeAssets, cfg.LinuxMuslRuntimeAssetsHash, "LinuxMuslRuntimeAssetsHash")
setupMu.Lock()
defer setupMu.Unlock()
if setupDone {
panic("Setup must only be called once")
}
if pathInitialized {
panic("Setup must be called before Path is accessed")
}
config = cfg
setupDone = true
}
var Path = sync.OnceValue(func() string {
setupMu.Lock()
defer setupMu.Unlock()
if !setupDone {
return ""
}
pathInitialized = true
path := install()
return path
})View on GitHub (pinned to cd8cf15dc3)