CherryHQ/cherry-studio · error · Error

Invalid credentials format in ${tokenPath}

Error message

Invalid credentials format in ${tokenPath}

What it means

Thrown by loadCredentials() in WeChatProtocol when the credentials file at tokenPath exists and parses as JSON but fails Zod validation against CredentialsSchema ({token:string, baseUrl:string, accountId:string, userId:string}). This is a persisted-state corruption: the file is present but its shape does not match what the bot expects. ENOENT (file missing) is handled separately and returns undefined — only a present-but-malformed file throws.

Source

Thrown at src/main/ai/channels/adapters/wechat/WeChatProtocol.ts:561

  return {
    from_user_id: '',
    to_user_id: userId,
    client_id: randomUUID(),
    message_type: MessageType.BOT,
    message_state: MessageState.FINISH,
    context_token: contextToken,
    item_list: [{ type: MessageItemType.TEXT, text_item: { text } }]
  }
}

// --------------- Auth ---------------

async function loadCredentials(tokenPath: string): Promise<Credentials | undefined> {
  try {
    const raw = await readFile(tokenPath, 'utf8')
    const result = CredentialsSchema.safeParse(JSON.parse(raw))
    if (!result.success) {
      throw new Error(`Invalid credentials format in ${tokenPath}`)
    }
    return result.data
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
      return undefined
    }
    throw error
  }
}

async function saveCredentials(credentials: Credentials, tokenPath: string): Promise<void> {
  await mkdir(path.dirname(tokenPath), { recursive: true, mode: 0o700 })
  await writeFile(tokenPath, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 })
  await chmod(tokenPath, 0o600)
}

async function clearCredentials(tokenPath: string): Promise<void> {
  await rm(tokenPath, { force: true })

View on GitHub (pinned to 726446b54c)

Solutions

  1. Delete the credentials file at tokenPath (it is at feature.agents.channels/weixin_bot_<channelId>.json per WeChatAdapter.ts:22) to force a fresh QR login.
  2. Validate the file contents manually — it must have all four string fields: token, baseUrl, accountId, userId.
  3. If this is a migration issue, write a one-time migration that backfills missing fields or removes the old file.
  4. Run WeixinBot.login({force:true}) to regenerate the credentials file from a new QR scan.

Example fix

// before — corrupt file blocks startup; user must find and delete it manually
const existing = await loadCredentials(options.tokenPath) // throws

// after — caller treats invalid credentials as 'no credentials' and re-logins
let existing: Credentials | undefined
try {
  existing = await loadCredentials(options.tokenPath)
} catch (e) {
  logger.warn('Credentials file corrupt, forcing re-login', { error: (e as Error).message })
  await rm(options.tokenPath, { force: true })
}
if (existing) return existing
// proceed to QR login
Defensive patterns

Strategy: validation

Validate before calling

// Validate the credentials file shape BEFORE handing it to loadCredentials,
// or treat loadCredentials failure as 'no credentials' and force re-login.
const CREDENTIALS_SHAPE = {
  token: 'string',
  baseUrl: 'string',
  accountId: 'string',
  userId: 'string'
} as const

function looksLikeCredentials(raw: unknown): boolean {
  if (typeof raw !== 'object' || raw === null) return false
  return Object.entries(CREDENTIALS_SHAPE).every(
    ([k, t]) => typeof (raw as Record<string, unknown>)[k] === t
  )
}

// Safer wrapper: corrupt file → force re-login instead of propagating the throw
async function loadCredentialsOrForceRelogin(tokenPath: string): Promise<Credentials | undefined> {
  try {
    return await loadCredentials(tokenPath)
  } catch (e) {
    logger.warn('Credentials file invalid, removing to force re-login', { error: (e as Error).message })
    await rm(tokenPath, { force: true })
    return undefined
  }
}

Type guard

// Reuse the Zod schema already defined in WeChatProtocol.ts:139
// CredentialsSchema = z.object({ token: z.string(), baseUrl: z.string(), accountId: z.string(), userId: z.string() })
function isValidCredentials(raw: unknown): raw is Credentials {
  return CredentialsSchema.safeParse(raw).success
}

Try / catch

// In the login path, treat an invalid credentials file as 'no credentials'
// rather than letting the throw propagate to the user as a hard error.
let existing: Credentials | undefined
try {
  existing = await loadCredentials(options.tokenPath)
} catch {
  existing = undefined // force QR login
}
if (existing) return existing

Prevention

When it happens

Trigger: readFile succeeds, JSON.parse succeeds (otherwise it would throw SyntaxError, not this message — but note the catch only re-throws non-ENOENT errors, so a SyntaxError from JSON.parse would propagate, not this message), then CredentialsSchema.safeParse fails. This means the JSON is valid but missing required fields or has wrong types: e.g. {token:'x'} without baseUrl/accountId/userId, or fields with null where strings are required.

Common situations: An older app version wrote a credentials file with a different schema (e.g. missing accountId added in a later migration); manual editing of the token file corrupted it; a partial write left a truncated JSON object (though that usually fails JSON.parse first); the file was overwritten by another process writing a different JSON structure.

Understand the failure class

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/e3d2afadd327097d. Report an issue: GitHub.