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

  1. Guard the call with sync.Once (var once sync.Once; once.Do(func(){ embeddedcli.Setup(cfg) })) so Setup runs exactly once per process.
  2. Move the single Setup call to main()/package init of the entrypoint and remove Setup calls from library internals.
  3. 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

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


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)