# How CustomModesManager Parses and Validates .roomodes Configuration Files in Roo Code

> Discover how CustomModesManager parses and validates .roomodes configuration files in Roo Code. Learn about YAML parsing, JSON fallback, Zod schema validation, and mode merging for efficient workspace setup.

- Repository: [Roo Code/Roo-Code](https://github.com/RooCodeInc/Roo-Code)
- Tags: internals
- Published: 2026-04-26

---

**The `CustomModesManager` class locates the `.roomodes` file at the workspace root, sanitizes invisible characters and BOM markers, attempts YAML parsing with a JSON fallback for resilience, validates the structure against the Zod-based `customModesSettingsSchema`, and merges project-specific modes with global definitions while giving `.roomodes` precedence.**

Roo Code enables project-specific AI behavior through custom modes defined in a hidden `.roomodes` configuration file. The `CustomModesManager` class in [`src/core/config/CustomModesManager.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/src/core/config/CustomModesManager.ts) orchestrates this entire lifecycle, from filesystem discovery to schema validation and runtime merging with global settings.

## Locating the .roomodes File at the Workspace Root

The manager first discovers whether a `.roomodes` file exists in the current workspace. The private `getWorkspaceRoomodes` method checks for the active workspace folder, resolves the absolute path using `getWorkspacePath()`, and verifies existence via `fileExistsAtPath`:

```typescript
private async getWorkspaceRoomodes(): Promise<string | undefined> {
  const workspaceFolders = vscode.workspace.workspaceFolders
  if (!workspaceFolders || workspaceFolders.length === 0) return undefined

  const workspaceRoot = getWorkspacePath()
  const roomodesPath = path.join(workspaceRoot, ROOMODES_FILENAME)
  const exists = await fileExistsAtPath(roomodesPath)
  return exists ? roomodesPath : undefined
}

```

*Source*: [`CustomModesManager.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/CustomModesManager.ts) (lines 93‑104)

This ensures the manager only attempts to load the file when it is physically present at the workspace root, preventing unnecessary file system errors.

## Sanitizing Raw Content Before Parsing

Before attempting to parse the file, the manager passes raw content through two sanitation stages to eliminate editor-introduced artifacts that commonly break YAML parsers.

### Removing Byte-Order Marks and Invisible Characters

The `loadModesFromFile` method pipes content through `stripBom()` to remove UTF-8 Byte-Order Marks, then applies `cleanInvisibleCharacters` to neutralize non-breaking spaces, zero-width spaces, smart quotes, and dash variants:

```typescript
private cleanInvisibleCharacters(content: string): string {
  return content.replace(CustomModesManager.PROBLEMATIC_CHARS_REGEX, match => {
    // mapping table (NBSP → space, ZWSP → "", smart quotes → ', etc.)
  })
}

```

*Source*: [`CustomModesManager.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/CustomModesManager.ts) (lines 15‑42)

This regex-based replacement ensures that text copied from word processors or web interfaces does not cause cryptic parse failures.

## Parsing YAML with JSON Fallback

The `parseYamlSafely` method implements a resilient two-phase parsing strategy. It first attempts standard YAML parsing; if that fails and the target is specifically `.roomodes`, it falls back to `JSON.parse` to accommodate users who prefer JSON syntax:

```typescript
private parseYamlSafely(content: string, filePath: string): any {
  let cleanedContent = stripBom(content)
  cleanedContent = this.cleanInvisibleCharacters(cleanedContent)

  try {
    const parsed = yaml.parse(cleanedContent)          // primary YAML parsing
    return parsed ?? {}
  } catch (yamlError) {
    // .roomodes gets a JSON fallback – useful for users who hand‑edit the file.
    if (filePath.endsWith(ROOMODES_FILENAME)) {
      try {
        return JSON.parse(content)                     // JSON fallback
      } catch {
        // surface a friendly error message
        const line = (yamlError?.message ?? '').match(/at line (\d+)/)?.[1] ?? "unknown"
        vscode.window.showErrorMessage(t("common:customModes.errors.yamlParseError", { line }))
        return {}
      }
    }
    // non‑roomodes files just log the error
    console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, yamlError)
    return {}
  }
}

```

*Source*: [`CustomModesManager.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/CustomModesManager.ts) (lines 44‑80)

This approach provides immediate user feedback with the specific line number causing the failure, while returning an empty object to prevent runtime crashes from malformed input.

## Validating Against customModesSettingsSchema

After parsing, raw data undergoes strict validation using `customModesSettingsSchema` from the `@roo-code/types` package. The manager uses Zod's `safeParse` method to catch schema violations without throwing exceptions:

```typescript
const result = customModesSettingsSchema.safeParse(settings)

if (!result.success) {
  console.error(`[CustomModesManager] Schema validation failed for ${filePath}:`, result.error)

  // .roomodes gets a user‑friendly modal with the list of issues.
  if (filePath.endsWith(ROOMODES_FILENAME)) {
    const issues = result.error.issues
      .map(issue => `• ${issue.path.join(".")}: ${issue.message}`)
      .join("\n")
    vscode.window.showErrorMessage(t("common:customModes.errors.schemaValidationError", { issues }))
  }
  return []
}

```

*Source*: [`CustomModesManager.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/CustomModesManager.ts) (lines 94‑107)

Validation failures surface as localized toast notifications containing bulleted lists of specific property errors (e.g., `customModes.0.slug: Required`). The method returns an empty array for invalid inputs, ensuring only well-formed modes reach the runtime state.

## Merging Project-Specific and Global Modes

When file system watchers detect changes, the manager loads both the global [`customModes.yaml`](https://github.com/RooCodeInc/Roo-Code/blob/main/customModes.yaml) and the project-specific `.roomodes`, then merges them using `mergeCustomModes`. Project definitions take precedence through a "first-wins" strategy on the `slug` property:

```typescript
// In the .roomodes watcher (see around L24‑31)
const settingsModes = await this.loadModesFromFile(settingsPath)
const roomodesModes = await this.loadModesFromFile(roomodesPath)

// .roomodes takes precedence
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
await this.context.globalState.update("customModes", mergedModes)

```

*Source*: [`CustomModesManager.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/CustomModesManager.ts) (lines 24‑34)

This precedence rule allows developers to override global mode behaviors for specific projects without modifying their global configuration.

## Retrieving the Final Mode List via Public API

The `getCustomModes` method serves as the canonical entry point for the rest of the extension (UI panels, command handlers, and webviews). It returns a cached list of merged modes annotated with their source (`"project"` or `"global"`):

```typescript
public async getCustomModes(): Promise<ModeConfig[]> {
  // Cache handling omitted for brevity
  const settingsPath = await this.getCustomModesFilePath()
  const settingsModes = await this.loadModesFromFile(settingsPath)

  const roomodesPath = await this.getWorkspaceRoomodes()
  const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []

  // Build merged list (project first)
  const mergedModes = [
    ...roomodesModes.map(m => ({ ...m, source: "project" as const })),
    ...settingsModes.filter(m => !roomodesModes.some(r => r.slug === m.slug))
      .map(m => ({ ...m, source: "global" as const }))
  ]

  await this.context.globalState.update("customModes", mergedModes)
  this.cachedModes = mergedModes
  return mergedModes
}

```

*Source*: [`CustomModesManager.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/CustomModesManager.ts) (lines 56‑95)

This method automatically handles cache invalidation via VS Code's file system watchers, ensuring consumers always receive the latest validated configuration without manual reloads.

## Summary

- **Discovery**: `getWorkspaceRoomodes` locates `.roomodes` at the workspace root using `getWorkspacePath()` and `fileExistsAtPath`.
- **Sanitization**: `cleanInvisibleCharacters` strips BOM markers and problematic Unicode characters before parsing.
- **Parsing**: `parseYamlSafely` attempts YAML parsing with a JSON fallback exclusively for `.roomodes` files.
- **Validation**: `customModesSettingsSchema.safeParse` enforces type safety, surfacing user-friendly error toasts with specific line issues.
- **Merging**: Project modes from `.roomodes` override global modes with matching `slug` values.
- **API**: `getCustomModes` provides a cached, source-annotated list to the rest of the Roo Code extension.

## Frequently Asked Questions

### What happens if my .roomodes file contains invalid YAML?

Roo Code catches the YAML parse error and attempts a JSON fallback parse (since `.roomodes` specifically supports both formats). If both fail, the extension displays a localized error toast indicating the specific line number causing the issue, and returns an empty mode list to prevent crashes.

### How does Roo Code resolve duplicate mode slugs between global and project settings?

When `mergeCustomModes` processes the combined list, it preserves the first occurrence of each mode based on the `slug` property. Because `.roomodes` modes are processed first in the merge array, they automatically override any global mode sharing the same identifier.

### Where is the schema for custom modes defined?

The `customModesSettingsSchema` and `modeConfigSchema` are defined in the `@roo-code/types` package (imported from [`src/core/config/CustomModesManager.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/src/core/config/CustomModesManager.ts)). These Zod schemas enforce required fields like `slug`, `name`, and `roleDefinition`, ensuring runtime type safety across the extension.

### Can I trigger UI updates when .roomodes changes without restarting VS Code?

Yes. The `CustomModesManager` constructor accepts an `onUpdate` callback that triggers automatically via internal file system watchers. When you save changes to `.roomodes`, the manager reloads, validates, and merges the configuration, then invokes the callback so webviews and panels can refresh their mode lists immediately.