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

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 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:

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 (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:

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

Source: 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:

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 (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:

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 (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 and the project-specific .roomodes, then merges them using mergeCustomModes. Project definitions take precedence through a "first-wins" strategy on the slug property:

// 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 (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"):

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 (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). 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →