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 | 約30 | ConfigPathsヘルパー:グローバル/プロジェクトレベルの設定ファイルのパス計算 |
src/config/markdown.ts | 約20 | ConfigMarkdown型:Markdown描画関連のアクティビティ設定 |
src/config/tui-schema.ts | 約50 | TUI設定Schema:キーボードショートカット、テーマ、レイアウトその他のUI設定のZod定義 |
src/config/tui.ts | 約100 | TUI設定ロジック:TUI関連設定のローディングと処理 |
src/config/tui-migrate.ts | 約60 | TUI設定マイグレーション:旧設定形式から新形式への自動マイグレーション |
src/config/console-state.ts | 約20 | ConsoleState型:コンソール出力の状態管理 |
.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-4はSessionPromptレイヤーで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 + 配列連結
複数層の設定マージでは、remedaのmergeDeepを使用し、特定のフィールドに対して配列連結を拡張しています:
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)
}
マージ動作のまとめ:
| フィールドタイプ | マージ戦略 | 例 |
|---|---|---|
単純値(model、temperature) | 後者が前者を上書き | プロジェクトのmodelがグローバルのmodelを上書き |
辞書(provider、mcp、agent) | 深いマージ、キー単位でマージ | グローバルのprovider.openai + プロジェクトのprovider.openai → マージ済み |
配列(plugins、instructions、command) | 連結、上書きなし | グローバルの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ではなくJSONC | JSONCは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:権限ルールは
PermissionSchemaを通じて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()時に自動的にクリーンアップされます。設定更新は完全なインスタンス再構築をトリガーします