What happened?
In packages/core/src/code_assist/oauth-credential-storage.ts, the migrateFromFileStorage() method reads the old credential file (~/.gemini/oauth_creds.json) and calls JSON.parse(credsJson) at line 131 without any try/catch.
The file-read error handling only catches ENOENT (file not found). If the credential file exists but contains corrupted/invalid JSON (e.g., partial write from a crash, disk error, manual edit), an unhandled SyntaxError propagates up and crashes the entire authentication flow.
// Current code (line 108-141):
private static async migrateFromFileStorage(): Promise<Credentials | null> {
const oldFilePath = path.join(homedir(), GEMINI_DIR, OAUTH_FILE);
let credsJson: string;
try {
credsJson = await fs.readFile(oldFilePath, 'utf-8');
} catch (error: unknown) {
if (/* ENOENT check */) {
return null;
}
throw error;
}
// ⚠️ No try/catch — corrupted JSON crashes the app
const credentials: Credentials = JSON.parse(credsJson);
await this.saveCredentials(credentials);
// ⚠️ Old file is deleted even if saveCredentials() throws above
await fs.rm(oldFilePath, { force: true }).catch(() => {});
return credentials;
}
Additionally, fs.rm(oldFilePath) at line 137 runs regardless of whether saveCredentials() succeeded. If saveCredentials() throws, the old file is already deleted — permanently losing the user's credentials.
What did you expect to happen?
- If
JSON.parse fails on a corrupted file, the error should be caught, a warning logged, and null returned (treat as "no credentials to migrate") — not crash the app.
- The old credential file should only be deleted after
saveCredentials() is confirmed to have succeeded.
Suggested Fix
private static async migrateFromFileStorage(): Promise<Credentials | null> {
const oldFilePath = path.join(homedir(), GEMINI_DIR, OAUTH_FILE);
let credsJson: string;
try {
credsJson = await fs.readFile(oldFilePath, 'utf-8');
} catch (error: unknown) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
return null;
}
throw error;
}
let credentials: Credentials;
try {
credentials = JSON.parse(credsJson) as Credentials;
} catch {
coreEvents.emitFeedback('warning', `Corrupted OAuth credential file at ${oldFilePath}, skipping migration`);
return null;
}
await this.saveCredentials(credentials);
// Only delete old file after saveCredentials() succeeded (we're past await above)
await fs.rm(oldFilePath, { force: true }).catch(() => {});
return credentials;
}
Client information
Client Information
Source code review — packages/core/src/code_assist/oauth-credential-storage.ts, lines 108-141.
Anything else we need to know?
What happened?
In
packages/core/src/code_assist/oauth-credential-storage.ts, themigrateFromFileStorage()method reads the old credential file (~/.gemini/oauth_creds.json) and callsJSON.parse(credsJson)at line 131 without any try/catch.The file-read error handling only catches
ENOENT(file not found). If the credential file exists but contains corrupted/invalid JSON (e.g., partial write from a crash, disk error, manual edit), an unhandledSyntaxErrorpropagates up and crashes the entire authentication flow.Additionally,
fs.rm(oldFilePath)at line 137 runs regardless of whethersaveCredentials()succeeded. IfsaveCredentials()throws, the old file is already deleted — permanently losing the user's credentials.What did you expect to happen?
JSON.parsefails on a corrupted file, the error should be caught, a warning logged, andnullreturned (treat as "no credentials to migrate") — not crash the app.saveCredentials()is confirmed to have succeeded.Suggested Fix
Client information
Client Information
Source code review —
packages/core/src/code_assist/oauth-credential-storage.ts, lines 108-141.Anything else we need to know?
console.error→coreEventsin the same file, but did not address theJSON.parsecrash or the file-deletion ordering.