コンテンツにスキップ

Config ソースコード解説

モジュール概要

Configモジュールは、OpenCodeのすべての設定を管理します。モデルの選択から権限ルール、MCPサーバーからキーボードショートカットまでを担当します。Zod Schema駆動の型システム6層構成の優先順位ベースのマージを組み合わせることで、プロジェクトレベルの共有設定とユーザーの個人設定を秩序正しく共存させることができます。

主要な設計上の選択:

  • 完全なZod Schemaカバレッジ:すべての型定義にZodを使用することで、ランタイムバリデーションとコンパイル時の型安全性を提供
  • 6層構成の設定優先順位:組み込みデフォルト → グローバル設定 → 環境変数ファイル → プロジェクト設定 → .opencode/ → 環境変数コンテンツ
  • JSONC + 変数置換:コメントをサポートするJSON形式テンプレートに加え、ランタイム置換のためのenv:VAR / file:pathプレースホルダー構文
  • mergeDeep + 配列連結:remedaベースの深いマージを行い、plugins / instructionsなどの配列フィールドは上書きではなく連結
  • 完全なホットリロード:設定変更時にInstance.dispose()Instance.state()で破棄-再構築サイクルを実行し、一貫性を確保

Configはまた、Effect.tsアーキテクチャに基づいて構築されており、Config.Serviceを通じてサービスインターフェースを公開し、InstanceStateを通じて各プロジェクトの設定ライフサイクルを管理します。

主要ファイル

ファイルパス行数責務
src/config/config.ts約2600モジュールのメインファイル:すべてのZod Schema定義、6層ローディングロジック、mergeDeepマージ、変数置換、Service定義
src/config/paths.ts約30ConfigPathsヘルパー:グローバル/プロジェクトレベルの設定ファイルのパス計算
src/config/markdown.ts約20ConfigMarkdown型:Markdown描画関連のアクティビティ設定
src/config/tui-schema.ts約50TUI設定Schema:キーボードショートカット、テーマ、レイアウトその他のUI設定のZod定義
src/config/tui.ts約100TUI設定ロジック:TUI関連設定のローディングと処理
src/config/tui-migrate.ts約60TUI設定マイグレーション:旧設定形式から新形式への自動マイグレーション
src/config/console-state.ts約20ConsoleState型:コンソール出力の状態管理
.opencode/config.jsonプロジェクトレベルの設定ファイル(Gitにコミット可能)
~/.config/opencode/config.jsonユーザーのグローバル設定

型システム

Permission Schema — 14以上の操作タイプ

権限操作型はConfigの中でもっともコアなSchemaの一つであり、Agentが実行できるすべての操作カテゴリを定義します:

// Permission action tri-state enum
const PermissionAction = z.enum(["ask", "allow", "deny"]);

// Permission rules: independent control over 14+ operations
const Permission = z.object({
  read: PermissionAction.default("allow"),               // Read files
  edit: PermissionAction.default("ask"),                 // Edit files
  bash: PermissionAction.default("ask"),                 // Execute Shell commands
  glob: PermissionAction.default("allow"),               // File pattern search
  grep: PermissionAction.default("allow"),               // File content search
  list: PermissionAction.default("allow"),               // Directory listing
  task: PermissionAction.default("allow"),               // Subtask execution
  external_directory: PermissionAction.default("ask"),   // Access directories outside project
  todowrite: PermissionAction.default("ask"),            // Write TODO
  todoread: PermissionAction.default("allow"),           // Read TODO
  question: PermissionAction.default("ask"),             // Ask user questions
  webfetch: PermissionAction.default("ask"),             // Network requests
  websearch: PermissionAction.default("ask"),            // Web search
  codesearch: PermissionAction.default("ask"),           // Code search
  lsp: PermissionAction.default("ask"),                  // LSP operations
  doom_loop: PermissionAction.default("ask"),            // Doom Loop confirmation
}).catchall(PermissionAction.default("ask"));

設計意図:デフォルトポリシーは「読み取り操作は許可、書き込み操作は確認」で、効率と安全性のバランスを取っています。catchallにより、新しく追加されたツールが誤って許可されることを防ぎます。リストにない操作タイプはデフォルトでaskフローに従います。

Provider Schema — Model Provider設定

const Provider = z.object({
  apiKey: z.string().optional(),          // API key (supports {env:} substitution)
  baseURL: z.string().optional(),         // Custom API endpoint
  disabled: z.boolean().optional(),       // Whether to disable this Provider
  models: z.record(z.string(), z.object({
    // Model-level override configuration
    disabled: z.boolean().optional(),
    // ...
  })).optional(),
});

Mcp Schema — MCP Server設定

const Mcp = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("local"),             // Local MCP (stdio mode)
    command: z.string(),                  // Launch command, e.g. "npx"
    args: z.array(z.string()).optional(), // Command arguments
    env: z.record(z.string()).optional(), // Environment variables
    disabled: z.boolean().optional(),
  }),
  z.object({
    type: z.literal("remote"),            // Remote MCP (SSE mode)
    url: z.string(),                      // SSE endpoint URL
    headers: z.record(z.string()).optional(),
    disabled: z.boolean().optional(),
  }),
]);

discriminatedUnionにより、両方のMCPモードが型安全を保ちながら共存できます。TypeScriptはtypeフィールドに基づいて残りのフィールドを自動的に絞り込む(narrowing)ことができます。

Agent Schema — Agentカスタム設定

const Agent = z.object({
  model: z.string().optional(),           // Bound model
  prompt: z.string().optional(),          // Custom system prompt
  permission: Permission.optional(),      // Override permission rules
  temperature: z.number().optional(),     // Temperature parameter
  topP: z.number().optional(),            // top_p parameter
  disabled: z.boolean().optional(),       // Whether disabled
  steps: z.number().int().positive().optional(), // Maximum steps
});

Command Schema — カスタムコマンド

const Command = z.object({
  description: z.string().optional(),     // Command description
  agent: z.string().optional(),           // Specify executing Agent
  template: z.string(),                   // Command template text
  subtask: z.boolean().optional(),        // Whether subtask mode
});

Info — トップレベル集約Schema

すべてのサブ設定はトップレベルのInfo Schemaに集約されます:

const Info = z.object({
  model: ModelSchema.optional(),          // Default model selection
  provider: z.record(Provider).optional(), // Provider configuration dictionary
  agent: z.record(Agent).optional(),      // Custom Agent configuration
  mcp: z.record(Mcp).optional(),          // MCP server configuration
  permission: Permission.optional(),      // Global permission rules
  command: z.array(Command).optional(),   // Custom command list
  keybinds: Keybinds.optional(),          // Keybindings
  tui: TUI.optional(),                    // TUI configuration
  server: Server.optional(),              // Server configuration
  // ... other fields
});

各サブSchemaは.default()を使用してフォールバック値を提供し、任意の設定項目が欠落している場合でもシステムが正常に機能することを保証します。Info.parse()は設定ローディングチェーンにおける最終ゲートです。Schemaに適合しない設定は、ここで明確なエラーメッセージとともに却下されます。

コアフロー

6層構成の設定ローディング優先順位

設定のローディングは単なる「ファイルの上書き」ではなく、6層の検出-マージプロセスです(優先順位は低から高):

Layer 1: Built-in Defaults
  │  Values defined by .default() in Schemas

Layer 2: Global Configuration Files
  │  ~/.config/opencode/config.json
  │  ~/.config/opencode/opencode.json
  │  ~/.config/opencode/opencode.jsonc

Layer 3: OPENCODE_CONFIG Environment Variable
  │  Config file path specified by environment variable
  │  Suitable for CI/CD scenarios or temporary overrides

Layer 4: Project Configuration (findUp)
  │  Searches upward from current directory for opencode.jsonc / opencode.json
  │  Can be committed to Git, shared by team members

Layer 5: .opencode/ Directory
  │  .opencode/config.json within the project
  │  Not committed to Git (should be in .gitignore), personal preferences

Layer 6: OPENCODE_CONFIG_CONTENT Environment Variable
  │  JSON content passed directly via environment variable (highest priority)
  │  Suitable for containerized deployments, avoiding config file mounting

注意:CLIフラグはマージチェーンには含まれません。コマンドライン引数はmergeDeepに参加するのではなく、呼び出し先で直接上書きします。例えば、--model anthropic/claude-sonnet-4SessionPromptレイヤーでConfigのモデル選択を直接上書きします。

ローディングとパース:JSONC + 変数置換

load()関数は、単一の設定ファイルに対する完全なローディングプロセスを処理します:

load(filePath)
  ├─ Read file content (Bun.file().text())
  ├─ JSONC parsing
  │   └─ jsonc-parser's parseJsonC()
  │      Supports single-line comments //, multi-line comments /* */, trailing commas
  ├─ Variable substitution (recursively traverse all string values)
  │   ├─ {env:VAR} → process.env.VAR
  │   │   Example: {env:OPENAI_API_KEY} → sk-xxxxx
  │   ├─ {file:path} → Read external file content
  │   │   Example: {file:./prompts/system.txt} → file content string
  │   └─ Unmatched environment variables → Keep original string and output warning
  └─ Return parsed raw object (not yet Zod-validated)

{env:VAR}置換により、APIキーなどの機密情報を環境変数に残し、プレーンテキストで設定ファイルに書き込むことを避けられます。{file:path}は外部ファイル(長いプロンプトテンプレートなど)のインポートをサポートし、設定ファイルを簡潔に保ちます。変数置換はZodバリデーションの前に実行されるため、Schemaは実際の置換後の値に対して型チェックを行えます。

マージ戦略:mergeDeep + 配列連結

複数層の設定マージでは、remedamergeDeepを使用し、特定のフィールドに対して配列連結を拡張しています:

function mergeConfigConcatArrays(base: Info, ...overrides: Info[]): Info {
  // Deep merge based on remeda.mergeDeep
  // Special behavior:
  //   - plugins, instructions, command and other array fields → concatenated, not overwritten
  //   - provider, mcp, agent and other dictionary fields → deep merged
  //   - model, temperature and other simple values → latter overrides former
  return overrides.reduce((acc, override) => {
    return customMerge(acc, override)
  }, base)
}

マージ動作のまとめ:

フィールドタイプマージ戦略
単純値(modeltemperature後者が前者を上書きプロジェクトのmodelがグローバルのmodelを上書き
辞書(providermcpagent深いマージ、キー単位でマージグローバルのprovider.openai + プロジェクトのprovider.openai → マージ済み
配列(pluginsinstructionscommand連結、上書きなしグローバルのplugins + プロジェクトのplugins = 統合リスト

これにより、プロジェクトレベルのpluginsがユーザーレベルのpluginsを上書きするのではなく、マージされます。両方の設定が相互に補完し合うことができます。

プラグインの重複排除:deduplicatePlugins

マージ後、重複するプラグインが存在する可能性があります(グローバルとプロジェクトの両方の設定が同じプラグインを宣言している場合など)。deduplicatePlugins()は正規化された名前で重複を排除します:

function deduplicatePlugins(plugins: Plugin[]): Plugin[] {
  // Group by canonical name
  // Keep the highest-priority one in each group (i.e., the plugin from the later-loaded config source)
  // Canonical name: package name with version and scope removed
  // Example: @scope/my-plugin@1.0.0 and @scope/my-plugin@2.0.0 are treated as the same
  const seen = new Map<string, Plugin>()
  for (const plugin of plugins) {
    const canonical = toCanonical(plugin.name)
    seen.set(canonical, plugin)  // Later writes override earlier ones
  }
  return [...seen.values()]
}

重複排除ルール:高優先順位の設定ソースからのプラグインが優先されます。plugins配列は優先順位低から高へと連結されるため、同じ名前のプラグインが後に出現した場合は自然に前のものを上書きします。

Config.Service — Effect Serviceアーキテクチャ

ConfigモジュールはEffect Serviceを通じてインターフェースを公開します:

export class Service extends Effect.Service<Service>("Config.Service")(
  undefined, // Service context definition
  () =>
    Effect.gen(function* () {
      // Initialize configuration state
      const cfg = yield* loadConfig()
      return {
        get: () => cfg,
        update: (newConfig) => updateConfig(newConfig),
        // ...
      }
    }),
) {}

上位モジュールはEffect.provide(Service.Default)を通じて設定サービスを注入し、Config.get()を通じて現在の設定にアクセスします。Effectの依存性注入により、設定はテストで簡単に置き換え可能になります。

InstanceStateキャッシュ

設定ローディングの結果はInstance.state()を通じてキャッシュされ、プロジェクトインスタンスのライフサイクルにバインドされます:

const state = Instance.state(async () => {
  // Six-layer config loading + merging + validation
  return await loadAllLayers()
})

export async function get() {
  return state()  // Executes the factory function on first call, returns cached result thereafter
}

Instance.state()は非同期関数を返します。この関数は最初の呼び出し時にファクトリ関数を実行し、結果をキャッシュします。キャッシュはInstance.dispose()が呼び出されると自動的にクリアされます。これは、設定更新時の「破棄-再構築」が完全な再ローディングをトリガーすることを意味します。

設定更新と完全なホットリロード

update()関数は「破棄-再構築」戦略を使用してホット設定更新を実装します:

update(newConfig)
  ├─ JSON.stringify(newConfig, null, 2)
  ├─ fs.writeFile(projectConfigPath, content)
  │  └─ Write to project-level .opencode/config.json
  ├─ Instance.dispose(currentState)
  │  └─ Destroy all caches for the current instance
  │     ├─ Config state cache cleanup
  │     ├─ Agent state cache cleanup
  │     ├─ Provider state cache cleanup
  │     └─ MCP connection closure
  └─ Instance.state()
     └─ Reload, triggering the full initialization chain:
        ├─ Load all configuration layers (six layers)
        ├─ mergeDeep merging
        ├─ deduplicatePlugins deduplication
        ├─ Zod Schema validation
        ├─ Install dependencies (plugins)
        ├─ Load commands/agents/modes/plugins (via glob pattern scanning)
        └─ Build new InstanceState

設計上のトレードオフ:増分更新よりも完全リロードを選択しました。リロード性能を犠牲にしていますが、複雑な状態同期の問題を回避しています。増分更新には「どのモジュールがどの設定項目に依存しているか」を追跡する必要がありますが、疎結合アーキテクチャではこれを維持するコストが非常に高くなります。実際には、リロードウィンドウは非常に短いです(通常100ms未満)。設定ローディング自体は単なるファイル読み取りとSchemaバリデーションだからです。

TUI設定とマイグレーション

TUI(Terminal UI)設定には独自のSchemaとマイグレーションロジックがあり、旧バージョンからの自動アップグレードをサポートしています:

// tui-migrate.ts — Automatic migration from old config format
function migrate(oldConfig: unknown): TUIConfig {
  // Detect old format and convert to new format
  // Example: old keybind field names mapped to new keybinds
  // Migration is auto-saved, transparent to the user
}

TUI設定には、キーボードショートカット、テーマカラー、パネルレイアウト、その他のUI詳細が含まれます。コアビジネス設定(モデル、権限)とはライフサイクルが異なるため、TUI設定は分離されています。

呼び出しチェーンの例

チェーン1:プロジェクト起動時の設定ローディング

ユーザーがプロジェクトディレクトリでopencodeを実行


Instance.state()  // First call, triggers configuration loading

  ├─ loadBuiltinDefaults()
  │   └─ Returns built-in defaults defined by .default() in Schemas

  ├─ loadGlobalConfig()
  │   └─ ConfigPaths.global() → ~/.config/opencode/
  │   └─ Attempt to read config.json / opencode.json / opencode.jsonc (by priority)
  │   └─ load(globalConfigPath) → JSONC parsing + variable substitution

  ├─ loadEnvConfig()
  │   └─ process.env.OPENCODE_CONFIG → config file at specified path
  │   └─ If exists → load(envConfigPath)

  ├─ loadProjectConfig()
  │   └─ findUp("opencode.jsonc", "opencode.json")
  │   └─ Search upward from cwd, stop at first match
  │   └─ load(projectConfigPath)

  ├─ loadOpencodeDir()
  │   └─ .opencode/config.json
  │   └─ load(opencodeDirPath)

  ├─ loadEnvContent()
  │   └─ process.env.OPENCODE_CONFIG_CONTENT → Parse JSON directly

  ├─ mergeConfigConcatArrays(defaults, global, env, project, opencodeDir, envContent)
  │   └─ Merge six layers from lowest to highest priority
  │   └─ Array fields concatenated, dictionary fields deep merged, simple values overwritten

  ├─ deduplicatePlugins(merged.plugins)
  │   └─ Deduplicate by canonical name

  ├─ Info.parse(result)
  │   └─ Zod Schema validation, type-safe configuration object

  └─ Store in InstanceState cache

チェーン2:APIキーの環境変数置換

設定ファイル opencode.jsonc:
{
  "provider": {
    "openai": {
      "apiKey": "{env:OPENAI_API_KEY}",  // Placeholder
      "baseURL": "{env:CUSTOM_OPENAI_URL}"
    }
  }
}


load(filePath)
  ├─ Read file content
  ├─ JSONC.Parse(content)
  │   └─ Supports // and /* */ comments
  │   └─ Result: { provider: { openai: { apiKey: "{env:OPENAI_API_KEY}", ... } } }
  ├─ Variable substitution (recursively traverse all string values)
  │   ├─ "{env:OPENAI_API_KEY}" → process.env.OPENAI_API_KEY → "sk-xxxxx"
  │   ├─ "{env:CUSTOM_OPENAI_URL}" → process.env.CUSTOM_OPENAI_URL → "https://..."
  │   └─ Result: { provider: { openai: { apiKey: "sk-xxxxx", baseURL: "https://..." } } }
  ├─ Provider Schema validation
  └─ Runtime apiKey is the actual value; no plaintext keys in config file

チェーン3:設定変更の伝播(完全リロード)

ユーザーがTUIでモデル選択を変更(claude-sonnet-4 → gpt-5)


Config.update({ model: "openai/gpt-5" })

  ├─ Read current project configuration
  ├─ Merge new value into configuration object
  ├─ fs.writeFile(projectConfigPath, JSON.stringify(newConfig, null, 2))

  ├─ Instance.dispose(currentState)
  │   ├─ Clear Config state cache
  │   ├─ Clear Provider state cache (close old SDK instances)
  │   ├─ Clear Agent state cache
  │   ├─ Close MCP server connections
  │   └─ Broadcast Instance disposed event

  └─ Instance.state() → Triggers full re-initialization
     ├─ Reload six-layer configuration
     ├─ Provider initialization: new model gpt-5 registered in Provider.Info
     ├─ Agent initialization: reads updated configuration
     ├─ MCP reconnection: starts MCP servers per new configuration
     └─ All modules depending on Config receive the new configuration

チェーン4:グローバル + プロジェクト設定のマージ

グローバル設定 ~/.config/opencode/config.json:
{
  "provider": {
    "anthropic": { "apiKey": "{env:ANTHROPIC_API_KEY}" },
    "openai": { "apiKey": "{env:OPENAI_API_KEY}" }
  },
  "plugins": ["plugin-a", "plugin-b"],
  "permission": {
    "bash": "ask"    // Global default: bash requires ask
  }
}

プロジェクト設定 ./opencode.jsonc:
{
  "provider": {
    "anthropic": {
      "disabled": true   // Project disables Anthropic
    }
  },
  "plugins": ["plugin-c"],
  "permission": {
    "bash": "allow",     // Project override: bash auto-allowed
    "edit": "allow"      // Project override: edit auto-allowed
  }
}


Merged result:
{
  provider: {
    anthropic: { apiKey: "sk-xxx", disabled: true },  // Deep merge
    openai: { apiKey: "sk-yyy" }
  },
  plugins: ["plugin-a", "plugin-b", "plugin-c"],      // Array concatenation
  permission: {
    bash: "allow",     // Project overrides global
    edit: "allow"      // Project addition
  }
}

チェーン5:外部ファイルインポート(file:pathプレースホルダー置換)

設定ファイル opencode.jsonc:
{
  "agent": {
    "custom-reviewer": {
      "prompt": "{file:./prompts/review-prompt.md}",  // Import external file
    }
  }
}

ファイル ./prompts/review-prompt.md の内容:
あなたはプロフェッショナルなコードレビュアーです。以下の側面を確認してください:
1. セキュリティの脆弱性
2. パフォーマンスの問題
3. コードスタイルの一貫性
...


変数置換後:
{
  agent: {
    "custom-reviewer": {
      prompt: "あなたはプロフェッショナルなコードレビュアーです。以下の側面を確認してください:\n1. セキュリティの脆弱性\n2. パフォーマンスの問題\n3. コードスタイルの一貫性\n..."
    }
  }
}

設計上のトレードオフ

決定事項理由
YAMLではなくJSONCJSONCはJSON互換性を保ちながらコメントをサポートし、より良いツールエコシステム(IDEの構文ハイライト、成熟したJSONCパーサー)を持ちます。YAMLのインデントへの厳格さと暗黙的な型変換は、バグを生みやすい
完全リロード vs. 増分更新完全リロードを選び、リロード性能を犠牲にして複雑な状態同期の問題を回避しています。増分更新には「どのモジュールがどの設定項目に依存しているか」を追跡する必要がありますが、疎結合アーキテクチャではこれは維持が非常にコストがかかります
Zod Schema駆動手動の型定義と比較して、Zodはランタイムバリデーションとコンパイル時の型推論の両方を提供します。1つのSchemaで二重の保証。z.infer<typeof Schema>はSchemaからTypeScriptの型を自動的に導出します
上書きではなく配列連結plugins/instructions/commandフィールドの連結動作により、グローバルとプロジェクトの設定が相互に排他的ではなく補完し合うことができます。上書きの場合、ユーザーはプロジェクト設定でグローバルな内容を再宣言する必要があります
無制限の継承ではなく6層6層の優先順位は一般的なユースケース(デフォルト、グローバル、CI、チーム共有、個人設定、コンテナ)をカバーします。さらに多くの層は認知負荷とデバッグの困難さを増加させます
findUpの上方向検索cwdから上方向に設定ファイルを検索することで、モノレポのサブディレクトリでopencodeを実行しても自動的にルート設定を見つけられます
catchallのデフォルトはask新規ツールタイプは明示的な設定の欠落により誤って許可されません。安全第一
InstanceStateキャッシュ設定ローディングは多層のファイル読み取りとSchemaバリデーションを含みます。キャッシュは繰り返しの手間を避けます。キャッシュのライフサイクルはプロジェクトインスタンスにバインドされ、プロジェクト切り替え時に自動的に無効化されます

他のモジュールとの関係

  • Provider:初期化時に、ProviderはConfigからモデル設定とAPIキーを読み取ります({env:VAR}変数置換後の実際の値)。Config.get().providerはProvider登録と認証情報を提供します
  • Permission:権限ルールはPermission Schemaを通じてConfigで定義されます。Permission.fromConfig()は設定をランタイムのRulesetsにパースします。Configのcatchallデフォルトは新しいツールタイプの安全性を保証します
  • Agent:Agent動作パラメータ(最大ループ数、デフォルトモデル、temperature、権限の上書きなど)はConfigのagentフィールドから読み取られます。AgentリストはInstance.state()を通じてキャッシュされます
  • MCP:MCPサーバーリスト(mcpフィールド)とその設定はConfigから来ます。local(stdio)とremote(SSE)の両モードをサポートしています。MCP設定の変更は再接続をトリガーします
  • Command:カスタムコマンドはConfigのcommand配列を通じて定義されます。組み込みコマンドとマージされ、Commandモジュールに登録されます
  • CLI / TUI:TUIキーボードショートカット(keybinds)、テーマ(tui)、その他のUI設定はConfigから来ます。TUI設定には独自のマイグレーショロジックがあります
  • Session:InstanceStateはSessionとライフサイクルを共有します。設定変更はSessionレベルのリロードをトリガーします。Sessionは初期化時にConfigを読み取ってモデルとAgent設定を決定します
  • Plugin:PluginリストはConfigからローディングされます。deduplicatePlugins()は各プラグインが一度だけローディングされることを保証します。Pluginの有効化/無効化は設定を通じて制御されます
  • Instance:ConfigキャッシュはInstance.state()を通じて管理され、Instance.dispose()時に自動的にクリーンアップされます。設定更新は完全なインスタンス再構築をトリガーします