github/copilot-sdk · error

Client is in Mode=ModeEmpty but neither BaseDirectory…

Error message

Client is in Mode=ModeEmpty but neither BaseDirectory, SessionFS, nor a URIConnection was supplied. Empty mode requires explicit, per-tenant storage; set ClientOptions.BaseDirectory or .SessionFS, or connect to an externally-managed runtime via URIConnection.

What it means

NewClient validates the requested mode before constructing the client. In Mode=ModeEmpty the library refuses to guess storage: empty mode demands explicit per-tenant storage via ClientOptions.BaseDirectory or ClientOptions.SessionFS, or an externally-managed runtime via a URIConnection. If none of the three is supplied the constructor panics with an actionable message instead of silently creating a client with unusable storage.

Solutions

  1. Set ClientOptions.BaseDirectory to a writable per-tenant directory path.
  2. Or supply a ClientOptions.SessionFS implementation for explicit session storage.
  3. Or pass a URIConnection in ClientOptions.Connection to attach to an externally-managed runtime.
  4. Or drop Mode: ModeEmpty and use the library's default mode if per-tenant storage is not actually required.

Example fix

// before
client := NewClient(ClientOptions{Mode: ModeEmpty})

// after
client := NewClient(ClientOptions{
    Mode:          ModeEmpty,
    BaseDirectory: "/var/lib/myapp/tenants/" + tenantID,
})
Defensive patterns

Strategy: validation

Validate before calling

func optsValidForEmptyMode(opts ClientOptions) bool {
    _, isURI := opts.Connection.(URIConnection)
    return opts.Mode != ModeEmpty || opts.BaseDirectory != "" || opts.SessionFS != nil || isURI
}

Type guard

func isURIConnection(c Connection) bool {
    _, ok := c.(URIConnection)
    return ok
}

Try / catch

defer func() {
    if r := recover(); r != nil {
        if s, ok := r.(string); ok && strings.Contains(s, "ModeEmpty") {
            log.Fatalf("empty-mode client needs storage: %s", s)
        }
        panic(r)
    }
}()

Prevention

When it happens

Trigger: Calling NewClient(ClientOptions{Mode: ModeEmpty}) with Connection not a URIConnection, SessionFS nil, and BaseDirectory empty — e.g. switching from default mode to ModeEmpty without adding storage options, or constructing a client with only credentials/tool options.

Common situations: Migrating existing code to multi-tenant ModeEmpty and forgetting BaseDirectory; copy-pasted options that dropped the SessionFS field; passing a plain (non-URI) Connection while intending an external runtime hookup.

Understand the failure class

Background: "X is required", "must be set", "cannot be empty": the missing-required-config error family, from Vertex AI project/location to WeChat keys — this error's family across 18 libraries.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/4d2d08d5489fda58. Report an issue: GitHub.

Appendix: source

Thrown at go/mode_empty.go:35

func validateNewClientForMode(opts *ClientOptions) {
	if opts == nil || opts.Mode != ModeEmpty {
		return
	}
	// Empty mode requires durable, app-owned storage. Either:
	//   - the app supplied a BaseDirectory the runtime can write to,
	//   - the app supplied a SessionFS implementation,
	//   - or the app is connecting to an externally-managed runtime via
	//     URIConnection (in which case the host owns storage).
	if opts.BaseDirectory != "" {
		return
	}
	if opts.SessionFS != nil {
		return
	}
	if _, ok := opts.Connection.(URIConnection); ok {
		return
	}
	panic("Client is in Mode=ModeEmpty but neither BaseDirectory, SessionFS, nor a URIConnection was supplied. " +
		"Empty mode requires explicit, per-tenant storage; set ClientOptions.BaseDirectory or .SessionFS, " +
		"or connect to an externally-managed runtime via URIConnection.")
}

// validateToolFilterList rejects bare "*" entries with an actionable error
// pointing at the [ToolSet] builder. Called for both availableTools and
// excludedTools.
func validateToolFilterList(field string, list []string) error {
	for _, entry := range list {
		if entry == "*" {
			return fmt.Errorf(
				"invalid %s entry %q: there is no bare wildcard. "+
					"Use one or more of NewToolSet().AddBuiltIn(\"*\"), .AddMCP(\"*\"), or .AddCustom(\"*\") "+
					"to target a specific source",
				field, entry)
		}
	}
	return nil

View on GitHub (pinned to cd8cf15dc3)