router-for-me/CLIProxyAPI · error
select Claude device ID: session ID is empty
Error message
select Claude device ID: session ID is empty
What it means
SelectDeviceID validates the Claude credential's device-ID pool and the conversation session ID before returning the device ID used for OAuth session binding. After normalizing the pool it trims the session ID and rejects an empty value, because Claude's API requires a non-empty session identifier to pin requests to a device. This is a pure input-validation error: nothing has touched the network yet when it fires.
Source
Thrown at internal/auth/claude/identity.go:274
seen[deviceID] = struct{}{}
deviceIDs = append(deviceIDs, deviceID)
}
if changed {
metadata[ClaudeDeviceIDsMetadataKey] = append([]string(nil), deviceIDs...)
}
return append([]string(nil), deviceIDs...), changed, nil
}
// SelectDeviceID returns the credential's sole device ID after validating the conversation session.
func SelectDeviceID(deviceIDs []string, sessionID string) (string, error) {
deviceIDs = NormalizeDeviceIDPool(deviceIDs)
if len(deviceIDs) != ClaudeDevicePoolSize {
return "", fmt.Errorf("select Claude device ID: device pool has %d entries, want %d", len(deviceIDs), ClaudeDevicePoolSize)
}
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return "", fmt.Errorf("select Claude device ID: session ID is empty")
}
return deviceIDs[0], nil
}
// ValidDeviceID reports whether a value matches Claude Code's lowercase 64-hex device format.
func ValidDeviceID(value string) bool {
if len(value) != claudeDeviceIDByteSize*2 || value != strings.ToLower(value) {
return false
}
decoded, errDecode := hex.DecodeString(value)
return errDecode == nil && len(decoded) == claudeDeviceIDByteSize
}
View on GitHub (pinned to 78f0c4079e)
Solutions
- Ensure the caller passes a non-empty conversation/session ID before invoking SelectDeviceID (e.g. default it to the request's conversation ID or a generated UUID).
- Trim the session ID at the point of intake so whitespace-only values are caught earlier with a clearer message.
- If session binding is optional for your flow, skip the SelectDeviceID call when no session exists instead of passing an empty string.
- Check upstream request parsing to confirm the session field name still matches after schema changes.
Example fix
// before
deviceID, err := claude.SelectDeviceID(pool.DeviceIDs, req.SessionID)
// after
sessionID := strings.TrimSpace(req.SessionID)
if sessionID == "" {
sessionID = req.ConversationID // or generate a UUID for anonymous use
}
deviceID, err := claude.SelectDeviceID(pool.DeviceIDs, sessionID) Defensive patterns
Strategy: validation
Validate before calling
sessionID := strings.TrimSpace(req.SessionID)
if sessionID == "" {
return fmt.Errorf("conversation requires a session ID")
}
deviceID, err := claude.SelectDeviceID(pool.DeviceIDs, sessionID) Type guard
func hasSessionID(id string) bool { return strings.TrimSpace(id) != "" } Prevention
- Default empty session IDs to the conversation ID or a generated UUID at intake.
- Trim session IDs from user/config input once, at the boundary.
When it happens
Trigger: Calling SelectDeviceID(deviceIDs, sessionID) where sessionID is "", consists only of whitespace, or was never populated (e.g. an empty conversation ID field in a request payload). It only fires after the pool itself already has exactly ClaudeDevicePoolSize entries, so the device pool is valid and the session ID is the sole problem.
Common situations: Passing a request's session/conversation ID straight from user input without defaulting it; refactoring a caller so the session ID variable is no longer set; testing with a hand-built credential and forgetting the session field; whitespace-padded session IDs copied from config files.
Related errors
- source auth path is empty
- auth path is empty
- auth id is empty
- fetch Claude OAuth %s: access token is empty
- failed to create directory: %v
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/2df9dcc6a14c0bc4.
Report an issue: GitHub.