Skip to content

perf: convert synchronous file reads to async in main process#111

Merged
matt1398 merged 1 commit intomatt1398:mainfrom
MintCollector:fix/async-file-reads
Mar 13, 2026
Merged

perf: convert synchronous file reads to async in main process#111
matt1398 merged 1 commit intomatt1398:mainfrom
MintCollector:fix/async-file-reads

Conversation

@MintCollector
Copy link
Copy Markdown
Contributor

@MintCollector MintCollector commented Mar 10, 2026

Summary

  • ConfigManager: loadConfig() and reload() converted from sync to async; adds initialize() and initializeInstance() factory for async startup
  • GitIdentityResolver: All methods (isWorktree, getRemoteUrl, detectWorktreeSource, getWorktreeDisplayName, getGitWorktreeName) converted from fs.*Sync to fs.promises.*
  • WorktreeGrouper: Updated to await the now-async GitIdentityResolver methods; uses Promise.all() for parallel worktree processing
  • utility.ts IPC handlers: handleShellOpenPath and handleReadMentionedFile converted from sync to async fs calls
  • Removes dead code: hasSubagentsSync() from SubagentLocator and ProjectScanner (zero callers in codebase)

Addresses #95

Test plan

  • All 653 tests pass
  • Typecheck clean
  • Lint clean
  • Production build succeeds
  • Verified hasSubagentsSync has zero callers before removal
  • App tested locally — no blank screen or runtime errors

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Refactor
    • Modernized file operation handling across project discovery, configuration management, and git integration services for improved reliability.
    • Configuration manager initialization now uses explicit asynchronous setup steps for better startup sequencing and state management.
    • Enhanced internal filesystem operation patterns throughout the application to ensure more robust file access handling.

Replace readFileSync, statSync, realpathSync, existsSync, and
readdirSync with their fs.promises equivalents to unblock the
Electron main process event loop.

- ConfigManager: make loadConfig() async, add initialize() +
  initializeInstance() pattern, make reload() async
- GitIdentityResolver: convert all methods (resolveIdentity,
  isWorktree, getRemoteUrl, getBranch, detectWorktreeSource,
  getWorktreeDisplayName, getGitWorktreeName) to use fs.promises
- SubagentLocator: remove hasSubagentsSync() (callers should use
  the existing async hasSubagents() instead)
- ProjectScanner: remove hasSubagentsSync() wrapper
- WorktreeGrouper: update call sites to await now-async methods
  using Promise.all for parallel worktree resolution
- utility.ts: convert handleShellOpenPath and handleReadMentionedFile
  to use fs.promises.access, stat, and readFile

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on a significant performance improvement by migrating synchronous file system operations to asynchronous ones within the main process. This change ensures that file I/O no longer blocks the event loop, leading to a more responsive and fluid user experience. The refactoring touches several key areas, including configuration management, Git identity resolution, and inter-process communication handlers, all aimed at enhancing the application's overall performance characteristics.

Highlights

  • Asynchronous File I/O Conversion: Converted numerous synchronous file system operations (e.g., fs.existsSync, fs.statSync, fs.readFileSync, fs.realpathSync) to their asynchronous fs.promises equivalents across multiple core services and IPC handlers, significantly improving application responsiveness and preventing blocking of the main process.
  • ConfigManager Refactoring: The ConfigManager was updated to support asynchronous loading and reloading of configuration. This includes making loadConfig() and reload() async, and introducing initialize() and initializeInstance() factory methods for proper async startup.
  • GitIdentityResolver Modernization: All methods within GitIdentityResolver that previously used synchronous file system calls were refactored to use fs.promises methods, making them asynchronous. This includes isWorktree, getRemoteUrl, detectWorktreeSource, getWorktreeDisplayName, and getGitWorktreeName.
  • WorktreeGrouper Adaptation: The WorktreeGrouper was updated to correctly await the now-asynchronous methods of GitIdentityResolver and utilizes Promise.all() for efficient parallel processing of worktrees.
  • Dead Code Removal: Removed the hasSubagentsSync() method from both SubagentLocator and ProjectScanner as it had no callers in the codebase, simplifying the codebase and removing unused functionality.
Changelog
  • src/main/ipc/utility.ts
    • Converted handleShellOpenPath to use fs.promises.access for path existence checks.
    • Converted handleReadMentionedFile to use fs.promises.access, fs.promises.stat, and fs.promises.readFile for file operations.
  • src/main/services/discovery/ProjectScanner.ts
    • Removed the unused hasSubagentsSync method.
  • src/main/services/discovery/SubagentLocator.ts
    • Removed the fs import as synchronous file operations were removed.
    • Removed the unused hasSubagentsSync method.
  • src/main/services/discovery/WorktreeGrouper.ts
    • Updated group.projects.map to use Promise.all to handle asynchronous worktree processing.
    • Modified calls to gitIdentityResolver.isWorktree, gitIdentityResolver.detectWorktreeSource, and gitIdentityResolver.getWorktreeDisplayName to await their now-asynchronous results.
  • src/main/services/infrastructure/ConfigManager.ts
    • Initialized this.config with a deep clone of DEFAULT_CONFIG in the constructor.
    • Added an initialize() method to asynchronously load the config from disk after construction.
    • Introduced initializeInstance() static method for asynchronous singleton initialization.
    • Made loadConfig() and reload() methods asynchronous.
    • Replaced fs.existsSync with fs.promises.access for config file existence checks.
    • Replaced fs.readFileSync with fs.promises.readFile for reading config content.
    • Exported configManagerPromise for asynchronous access to the initialized ConfigManager.
  • src/main/services/parsing/GitIdentityResolver.ts
    • Converted findRepoId to use fs.promises.access, fs.promises.stat, fs.promises.readFile, and fs.promises.realpath.
    • Made isWorktree method asynchronous and updated its file system checks to use fs.promises.stat.
    • Made getRemoteUrl method asynchronous and updated its file system checks to use fs.promises.access and fs.promises.readFile.
    • Made detectWorktreeSource method asynchronous and updated its file system checks to use fs.promises.access.
    • Made getWorktreeDisplayName method asynchronous.
    • Made getGitWorktreeName method asynchronous and updated its file system checks to use fs.promises.stat and fs.promises.readFile.
Activity
  • All 653 tests passed, ensuring no regressions were introduced.
  • Typecheck was clean, confirming type safety of the asynchronous changes.
  • Linting passed, maintaining code quality standards.
  • Production build succeeded, verifying the integrity of the build process.
  • Verification confirmed that hasSubagentsSync had zero callers before its removal.
  • Local application testing showed no blank screens or runtime errors, indicating stability.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai bot added the feature request New feature or request label Mar 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

Systematic migration of filesystem operations from synchronous to asynchronous patterns across discovery services, IPC utilities, configuration management, and git identity resolution. Removed synchronous method variants and introduced async initialization workflows with promise-based I/O.

Changes

Cohort / File(s) Summary
IPC Utilities
src/main/ipc/utility.ts
Replaced synchronous filesystem checks (fs.existsSync, fs.statSync, fs.readFileSync) with async promises (fs.promises.access, fs.promises.stat, fs.promises.readFile) in shell path and file reading handlers.
Discovery Services
src/main/services/discovery/ProjectScanner.ts, src/main/services/discovery/SubagentLocator.ts, src/main/services/discovery/WorktreeGrouper.ts
Removed synchronous hasSubagentsSync() methods; migrated SubagentLocator to use injected FileSystemProvider with async filesystem operations; updated WorktreeGrouper to use Promise.all for async worktree property resolution (isWorktree, detectWorktreeSource, getWorktreeDisplayName).
Configuration Management
src/main/services/infrastructure/ConfigManager.ts
Introduced async initialization pattern: new initialize() and static initializeInstance() methods; converted reload() and loadConfig() to async; added singleton exports configManagerPromise and configManager for structured startup sequencing.
Git Identity Resolution
src/main/services/parsing/GitIdentityResolver.ts
Migrated all filesystem operations to async: isWorktree(), getRemoteUrl(), detectWorktreeSource(), getWorktreeDisplayName(), and getGitWorktreeName() now return promises; added explicit error handling and fallback logic for worktree detection and remote URL retrieval.

Suggested labels

feature request


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/main/ipc/utility.ts (1)

205-218: Async conversion looks good; consider removing redundant access() check.

The fs.promises.access() call on lines 205-209 is redundant since fs.promises.stat() on line 212 will throw ENOENT if the file doesn't exist. The outer try/catch already handles this case.

♻️ Optional: Simplify by removing the access() check
     const safePath = validation.normalizedPath!;
 
-    // Check if file exists
-    try {
-      await fs.promises.access(safePath);
-    } catch {
-      return null;
-    }
-
     // Check if it's a file (not directory)
-    const stats = await fs.promises.stat(safePath);
+    let stats: fs.Stats;
+    try {
+      stats = await fs.promises.stat(safePath);
+    } catch {
+      return null;
+    }
     if (!stats.isFile()) {
       return null;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/utility.ts` around lines 205 - 218, Remove the redundant
fs.promises.access(safePath) check: in the function using safePath, delete the
try/catch that calls fs.promises.access and rely on fs.promises.stat(safePath)
to throw if the path doesn't exist; keep the subsequent isFile() check and the
readFile(safePath, 'utf8') call so behavior and error handling remain the same,
only eliminating the unnecessary access() invocation.
src/main/services/parsing/GitIdentityResolver.ts (3)

55-64: Consider consolidating existence check with stat operation.

The current pattern performs two async filesystem operations: access() to check existence, then stat(). You could simplify to a single stat() call, catching ENOENT to handle the non-existence case.

♻️ Optional: Single stat call with error handling
-      let gitPathExists = false;
-      try {
-        await fs.promises.access(gitPath);
-        gitPathExists = true;
-      } catch {
-        // Path doesn't exist
-      }
-
-      if (gitPathExists) {
-        const stats = await fs.promises.stat(gitPath);
+      let stats: fs.Stats | null = null;
+      try {
+        stats = await fs.promises.stat(gitPath);
+      } catch {
+        // Path doesn't exist or is inaccessible
+      }
+
+      if (stats) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/parsing/GitIdentityResolver.ts` around lines 55 - 64, The
code in GitIdentityResolver currently calls fs.promises.access(gitPath) and then
fs.promises.stat(gitPath); consolidate these into a single
fs.promises.stat(gitPath) call and catch errors: replace the two-step existence
check with one try/catch around stat(gitPath), treat ENOENT as "path doesn't
exist" (skip the stat result) and rethrow or handle other errors; update any
logic that used gitPathExists to use the presence/absence of the stat result.

266-279: Redundant nested try/catch blocks.

The nested try/catch structure at lines 268-273 inside 266-276 is redundant since both catch blocks handle errors the same way (ignore and continue). This can be simplified.

♻️ Optional: Simplify error handling
     // Fallback: check filesystem if available
     try {
       const gitPath = path.join(projectPath, '.git');
-      try {
-        const stats = await fs.promises.stat(gitPath);
-        return stats.isFile();
-      } catch {
-        // Path doesn't exist
-      }
+      const stats = await fs.promises.stat(gitPath);
+      return stats.isFile();
     } catch {
-      // Ignore errors - filesystem might not be available
+      // Path doesn't exist or filesystem not available
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/parsing/GitIdentityResolver.ts` around lines 266 - 279, The
nested try/catch in GitIdentityResolver (the function that checks the .git path)
is redundant; replace the two-level try/catch with a single try/catch: compute
gitPath (path.join(projectPath, '.git')), await fs.promises.stat(gitPath),
return stats.isFile() in the try block, and in the single catch block return
false (or fall through to return false) so errors are handled once and the outer
try/catch is removed.

537-552: Same redundant nested try/catch pattern.

Similar to isWorktree, the nested try/catch blocks here can be simplified.

♻️ Optional: Simplify error handling
     // Check if it's a standard git repo (only if filesystem exists)
     // For deleted repos, we'll return 'git' as fallback since we can't verify
     try {
       const gitPath = path.join(projectPath, '.git');
-      try {
-        await fs.promises.access(gitPath);
-        return 'git';
-      } catch {
-        // Path doesn't exist
-      }
+      await fs.promises.access(gitPath);
+      return 'git';
     } catch {
-      // Ignore errors - filesystem might not be available
+      // Path doesn't exist or filesystem not available
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/parsing/GitIdentityResolver.ts` around lines 537 - 552, The
nested try/catch around checking fs.promises.access(gitPath) is redundant;
simplify by using a single try/catch that attempts await
fs.promises.access(gitPath) and returns 'git' on success, and otherwise falls
through to the final return 'git'. Modify the block in GitIdentityResolver that
builds gitPath and calls fs.promises.access (remove the outer try/catch and keep
a single try/catch around the access call), ensuring any filesystem errors are
caught once and ignored so the function still ultimately returns 'git'.
src/main/services/infrastructure/ConfigManager.ts (1)

414-422: Consider converting persistConfig to async for consistency.

The PR converts reads to async but persistConfig still uses synchronous fs.existsSync, fs.mkdirSync, and fs.writeFileSync. These can block the event loop during config saves, which happen on every update.

♻️ Suggested async conversion
-  private persistConfig(config: AppConfig): void {
+  private async persistConfig(config: AppConfig): Promise<void> {
     const configDir = path.dirname(this.configPath);
-    if (!fs.existsSync(configDir)) {
-      fs.mkdirSync(configDir, { recursive: true });
-    }
+    await fs.promises.mkdir(configDir, { recursive: true });

     const content = JSON.stringify(config, null, 2);
-    fs.writeFileSync(this.configPath, content, 'utf8');
+    await fs.promises.writeFile(this.configPath, content, 'utf8');
   }

Note: This would require updating saveConfig() and all callers to handle the async nature, which may be out of scope for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/infrastructure/ConfigManager.ts` around lines 414 - 422,
persistConfig currently uses blocking fs calls (fs.existsSync, fs.mkdirSync,
fs.writeFileSync) which can block the event loop; convert persistConfig to an
async function that returns Promise<void>, use fs.promises (or import { promises
as fs } ) to await fs.mkdir(path.dirname(this.configPath), { recursive: true })
and await fs.writeFile(this.configPath, JSON.stringify(config, null, 2),
'utf8'), handling errors with try/catch and rethrow or log as appropriate;
update saveConfig and any callers to await the new async persistConfig (or
return the Promise) so callers handle async behavior consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/services/infrastructure/ConfigManager.ts`:
- Around line 353-362: The race occurs because ConfigManager.instance is
assigned after awaiting instance.initialize() in initializeInstance; change
initializeInstance (the static method) to set ConfigManager.instance = instance
before awaiting instance.initialize() so the same singleton is returned by
getInstance() and referenced by the exported configManager/configManagerPromise;
keep calling await instance.initialize() afterwards to perform async load (and
do not create another instance inside getInstance()).
- Around line 970-974: The exported promise configManagerPromise (from
ConfigManager.initializeInstance) is never awaited, so startup calls to
configManager.getConfig() read defaults instead of disk values; update startup
to await configManagerPromise before any synchronous access (i.e., before calls
to configManager.getConfig() in your app.whenReady handler) so initialization
completes, and keep the configManager export for convenience, and also update
the misleading comment for the configManager export to reflect that the instance
must be awaited (or alternatively remove the unused configManagerPromise export
if you prefer synchronous-only usage).

---

Nitpick comments:
In `@src/main/ipc/utility.ts`:
- Around line 205-218: Remove the redundant fs.promises.access(safePath) check:
in the function using safePath, delete the try/catch that calls
fs.promises.access and rely on fs.promises.stat(safePath) to throw if the path
doesn't exist; keep the subsequent isFile() check and the readFile(safePath,
'utf8') call so behavior and error handling remain the same, only eliminating
the unnecessary access() invocation.

In `@src/main/services/infrastructure/ConfigManager.ts`:
- Around line 414-422: persistConfig currently uses blocking fs calls
(fs.existsSync, fs.mkdirSync, fs.writeFileSync) which can block the event loop;
convert persistConfig to an async function that returns Promise<void>, use
fs.promises (or import { promises as fs } ) to await
fs.mkdir(path.dirname(this.configPath), { recursive: true }) and await
fs.writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf8'), handling
errors with try/catch and rethrow or log as appropriate; update saveConfig and
any callers to await the new async persistConfig (or return the Promise) so
callers handle async behavior consistently.

In `@src/main/services/parsing/GitIdentityResolver.ts`:
- Around line 55-64: The code in GitIdentityResolver currently calls
fs.promises.access(gitPath) and then fs.promises.stat(gitPath); consolidate
these into a single fs.promises.stat(gitPath) call and catch errors: replace the
two-step existence check with one try/catch around stat(gitPath), treat ENOENT
as "path doesn't exist" (skip the stat result) and rethrow or handle other
errors; update any logic that used gitPathExists to use the presence/absence of
the stat result.
- Around line 266-279: The nested try/catch in GitIdentityResolver (the function
that checks the .git path) is redundant; replace the two-level try/catch with a
single try/catch: compute gitPath (path.join(projectPath, '.git')), await
fs.promises.stat(gitPath), return stats.isFile() in the try block, and in the
single catch block return false (or fall through to return false) so errors are
handled once and the outer try/catch is removed.
- Around line 537-552: The nested try/catch around checking
fs.promises.access(gitPath) is redundant; simplify by using a single try/catch
that attempts await fs.promises.access(gitPath) and returns 'git' on success,
and otherwise falls through to the final return 'git'. Modify the block in
GitIdentityResolver that builds gitPath and calls fs.promises.access (remove the
outer try/catch and keep a single try/catch around the access call), ensuring
any filesystem errors are caught once and ignored so the function still
ultimately returns 'git'.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c7b952ff-8cb0-4b21-83e0-53a5d3be505b

📥 Commits

Reviewing files that changed from the base of the PR and between f23c581 and 90fd641.

📒 Files selected for processing (6)
  • src/main/ipc/utility.ts
  • src/main/services/discovery/ProjectScanner.ts
  • src/main/services/discovery/SubagentLocator.ts
  • src/main/services/discovery/WorktreeGrouper.ts
  • src/main/services/infrastructure/ConfigManager.ts
  • src/main/services/parsing/GitIdentityResolver.ts
💤 Files with no reviewable changes (2)
  • src/main/services/discovery/SubagentLocator.ts
  • src/main/services/discovery/ProjectScanner.ts

Comment on lines +353 to +362
/**
* Creates and initializes the singleton instance asynchronously.
* Loads configuration from disk. Call this at app startup.
*/
static async initializeInstance(configPath?: string): Promise<ConfigManager> {
const instance = new ConfigManager(configPath);
await instance.initialize();
ConfigManager.instance = instance;
return instance;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Race condition: ConfigManager.instance is set after await, causing dual instances.

The assignment at line 360 happens after the await on line 359. When the module loads, both exports (lines 970-974) are evaluated synchronously. Since initializeInstance() returns a Promise immediately (before line 360 executes), the subsequent getInstance() call finds ConfigManager.instance still null and creates a second instance.

This means configManager and configManagerPromise resolve to different instances—the synchronous export will never be initialized from disk.

🐛 Proposed fix: set instance before await
   static async initializeInstance(configPath?: string): Promise<ConfigManager> {
     const instance = new ConfigManager(configPath);
+    ConfigManager.instance = instance;
     await instance.initialize();
-    ConfigManager.instance = instance;
     return instance;
   }

This ensures both exports reference the same instance. The synchronous configManager will have defaults initially but will be updated in-place when initialize() completes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Creates and initializes the singleton instance asynchronously.
* Loads configuration from disk. Call this at app startup.
*/
static async initializeInstance(configPath?: string): Promise<ConfigManager> {
const instance = new ConfigManager(configPath);
await instance.initialize();
ConfigManager.instance = instance;
return instance;
}
/**
* Creates and initializes the singleton instance asynchronously.
* Loads configuration from disk. Call this at app startup.
*/
static async initializeInstance(configPath?: string): Promise<ConfigManager> {
const instance = new ConfigManager(configPath);
ConfigManager.instance = instance;
await instance.initialize();
return instance;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/infrastructure/ConfigManager.ts` around lines 353 - 362,
The race occurs because ConfigManager.instance is assigned after awaiting
instance.initialize() in initializeInstance; change initializeInstance (the
static method) to set ConfigManager.instance = instance before awaiting
instance.initialize() so the same singleton is returned by getInstance() and
referenced by the exported configManager/configManagerPromise; keep calling
await instance.initialize() afterwards to perform async load (and do not create
another instance inside getInstance()).

Comment on lines +970 to 974
/** Singleton instance promise - resolves to an initialized ConfigManager */
export const configManagerPromise = ConfigManager.initializeInstance();

/** Singleton instance for convenience (synchronous access, uses defaults until initialized) */
export const configManager = ConfigManager.getInstance();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for configManager usage in main process startup code
rg -n -C3 'configManager\.(getConfig|getInstance)' src/main/

Repository: matt1398/claude-devtools

Length of output: 7904


🏁 Script executed:

#!/bin/bash
# Check if configManagerPromise is awaited before app initialization
rg -n -C5 'configManagerPromise' src/main/

Repository: matt1398/claude-devtools

Length of output: 1036


🏁 Script executed:

cat -n src/main/index.ts | head -600 | tail -300

Repository: matt1398/claude-devtools

Length of output: 11810


🏁 Script executed:

# Search for app.on('ready') and related event handlers
rg -n "app\.on\(" src/main/index.ts -A 10

Repository: matt1398/claude-devtools

Length of output: 514


🏁 Script executed:

# Check the full context around createWindow and initializeServices calls
rg -n "createWindow\(\)|initializeServices\(\)" src/main/index.ts -B 5 -A 5

Repository: matt1398/claude-devtools

Length of output: 1798


Promise never awaited—startup uses uninitialized config with misleading comment.

The configManagerPromise is created but never awaited anywhere in the codebase. Startup code (lines 336, 428, 566 in src/main/index.ts) calls configManager.getConfig() synchronously during app.whenReady() without waiting for configManagerPromise to resolve, meaning disk configuration is never loaded. The comment on line 973 claiming the instance "uses defaults until initialized" is misleading—initialization never occurs since the promise is ignored.

Early startup (service initialization, window creation, and config application) all access the synchronous instance before any async initialization completes. Either await configManagerPromise at the start of the ready handler, or remove the unused promise export.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/infrastructure/ConfigManager.ts` around lines 970 - 974,
The exported promise configManagerPromise (from
ConfigManager.initializeInstance) is never awaited, so startup calls to
configManager.getConfig() read defaults instead of disk values; update startup
to await configManagerPromise before any synchronous access (i.e., before calls
to configManager.getConfig() in your app.whenReady handler) so initialization
completes, and keep the configManager export for convenience, and also update
the misleading comment for the configManager export to reflect that the instance
must be awaited (or alternatively remove the unused configManagerPromise export
if you prefer synchronous-only usage).

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request aims to significantly improve the application's performance and responsiveness by converting numerous synchronous file I/O operations to asynchronous ones using fs.promises. However, a critical security vulnerability was identified in the IPC handlers for utility operations, specifically shell:openPath and read-mentioned-file. These handlers trust the projectRoot parameter from the renderer process, allowing a compromised renderer to bypass path validation and access arbitrary files. It is recommended to resolve project paths using authoritative data in the main process. Beyond this, the changes are well-implemented, introducing initialize() and initializeInstance() in ConfigManager for asynchronous configuration loading, utilizing Promise.all() in WorktreeGrouper for parallel processing, and cleaning up dead code by removing unused hasSubagentsSync() methods.

// Check if path exists
if (!fs.existsSync(safePath)) {
try {
await fs.promises.access(safePath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The handleShellOpenPath IPC handler trusts the projectRoot parameter provided by the renderer process. This parameter is used in validateOpenPath to determine if the targetPath is within an allowed directory. A compromised renderer could provide a broad projectRoot (e.g., / on POSIX or C:\ on Windows) to bypass this restriction and open arbitrary files or directories on the system (excluding those explicitly blocked by SENSITIVE_PATTERNS).

To remediate this, the main process should maintain an authoritative mapping of project IDs to their respective root paths and use this mapping to resolve and validate paths, rather than trusting a path provided directly by the renderer.

Comment on lines +206 to +218
await fs.promises.access(safePath);
} catch {
return null;
}

// Check if it's a file (not directory)
const stats = fs.statSync(safePath);
const stats = await fs.promises.stat(safePath);
if (!stats.isFile()) {
return null;
}

// Read file content
const content = fs.readFileSync(safePath, 'utf8');
const content = await fs.promises.readFile(safePath, 'utf8');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The handleReadMentionedFile IPC handler trusts the projectRoot parameter provided by the renderer process. This parameter is passed to validateFilePath, which uses it to verify if the absolutePath is within an allowed directory. An attacker in the renderer process could provide a malicious projectRoot (e.g., /) to read arbitrary files on the system that are not covered by the SENSITIVE_PATTERNS list.

Remediation: Do not trust the projectRoot provided via IPC. Instead, resolve the project root path in the main process based on a trusted project identifier.

@matt1398 matt1398 merged commit 6a655b9 into matt1398:main Mar 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants