April 16, 2026 • バージョン: 2026.3.23-2

[ファイルバックドSecretRef設定でopenclaw statusが失敗する - プラグイン読み込み時に未解決の生の設定を再読み込みするため] - openclaw status Fails on File-Backed SecretRef Configs Due to Plugin Loading Re-Reading Unresolved Raw Config

チャネルシークレットがファイルバックドのSecretRefに移行されると、`openclaw status`コマンドはプラグイン登録中にクラッシュします。これは`ensurePluginRegistryLoaded()`がゲートウェイランタイムからの既に解決済みの設定を使用する代わりに、未解決の生の設定を再読み込みするためです。

🔍 症状

主なエラーの発生状況

ファイルバックドSecretRefを使用して channel secrets を使用する設定に対して openclaw status を実行すると、プラグイン登録中にCLIがクラッシュします:

$ openclaw status
[plugins] feishu failed during register ... Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret". Resolve this command against an active gateway runtime snapshot before reading it.
[openclaw] Failed to start CLI: PluginLoadFailureError: plugin load failed: feishu ...

正常に動作する並行コマンド

興味深いことに、同じ環境内の関連コマンドは正常に動作します:

$ openclaw health --json
{"status":"healthy","gateway":"connected","timestamp":"2026-03-23T14:32:15.421Z"}

$ openclaw status --json
{"channels":[{"name":"feishu","status":"active","connected":true}],"gateway":"connected"}

環境コンテキスト

この失敗は以下の特定の条件下で発生します:

  • OS: Linux `6.17.0-19-generic`
  • Node.js: `v22.22.1`
  • インストール方法: `~/.npm-global/lib/node_modules/openclaw` へのグローバルパッケージインストール
  • OpenClawバージョン: `2026.3.23-2`

SecretRef設定パターン

失敗する設定では、~/.openclaw/openclaw.json に以下のSecretRef構造を使用します:

{
  "channels": {
    "feishu": {
      "appId": "cli-app-01",
      "appSecret": {
        "source": "file",
        "provider": "localfile",
        "id": "/channels/feishu/appSecret"
      }
    }
  }
}

実際のシークレット値は ~/.openclaw/secrets.json に保存されています:

{
  "/channels/feishu/appSecret": "fl.1234567890abcdef..."
}

🧠 原因

アーキテクチャの概要

この失敗は、プラグイン読み込みとコマンドレベルのSecretRef解決の間の設定ライフサイクルの不一致に起因します。CLIアーキテクチャには2つの競合する設定消費パスがあります:

  1. コマンドレベルの解決: `status` コマンドはアクティブなゲートウェイランタイムスナップショット経由でSecretRefを解決する
  2. ルートレベルのプラグイン読み込み: ルートは未解決の設定を使用してプラグインをプリロードする

問題1: ルートレベルでの過早なプラグインプリロード

src/cli/program/routes.ts では、ルート設定が以下のように指定されています:

// Before (problematic)
loadPlugins: (argv) => !hasFlag(argv, "--json"),

これにより、status コマンドの前にプラグインが読み込まれます。プラグイン登録フェーズでは、まだ未解決のSecretRef形式のままのチャンネル認証情報にアクセスしようとします。

問題2: プラグインレジストリが常に生の設定を読み取る

src/cli/plugin-registry.tsensurePluginRegistryLoaded() 関数は、無条件に loadConfig() を呼び出します:

// src/cli/plugin-registry.ts
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
  if (registryState.loaded) {
    return;
  }
  
  // This ALWAYS re-reads from disk, discarding any resolved config
  const config = loadConfig();  // ← BUG: ignores resolved config passed via options
  
  await loadPlugins(config, options?.scope);
  registryState.loaded = true;
}

関数のシグネチャは options パラメータを受け取りしますが、設定の上書きには使用しません:

interface PluginLoadOptions {
  scope?: "all" | "configured-channels" | "core-only";
  config?: ResolvedConfig;  // ← This parameter exists but is unused
}

問題3: Statusコマンド解決の実行タイミングが迟い

src/commands/status.tsstatus コマンドは、ゲートウェイ経由でSecretRefを正しく解決します:

// src/commands/status.ts (simplified)
export async function statusCommand(argv: StatusArgs): Promise {
  // Step 1: Resolve config via gateway (this works correctly)
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  // Step 2: Load plugins (CRASHES HERE - cfg is not passed)
  ensurePluginRegistryLoaded({ scope: "configured-channels" });  // ← Missing config: cfg
  
  // Step 3: Display status (never reached)
  await renderStatus(cfg);
}

実行タイムライン比較

フェーズopenclaw health --jsonopenclaw status
ルートプラグインプリロードスキップ (--json あり)クラッシュをトリガー
コマンドハンドラ実行設定を解決設定を解決
プラグイン登録解決済み設定を使用未解決設定を使用

--json フラグが動作する理由

--json フラグは、ルート設定で条件付きにプラグイン読み込みをスキップするため、プラグインのプリロードをバイパスします:

loadPlugins: (argv) => !hasFlag(argv, "--json"),

--json を付けると、コマンドハンドラが設定を解決した後までプラグイン読み込みが遅延され、ensurePluginRegistryLoaded() が正しい設定コンテキストを受け取れるようになります。

🛠️ 解決手順

フェーズ1: 設定の上書きを受け入れるようにプラグインレジストリを修正

ファイル: src/cli/plugin-registry.ts

変更: 既存だが未使用の config オプションパラメータを使用する。

// Before
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
  if (registryState.loaded) {
    return;
  }
  
  const config = loadConfig();  // Always raw config
  
  await loadPlugins(config, options?.scope);
  registryState.loaded = true;
}

// After
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
  if (registryState.loaded) {
    return;
  }
  
  const config = options?.config ?? loadConfig();  // Use provided resolved config or fallback
  
  await loadPlugins(config, options?.scope);
  registryState.loaded = true;
}

フェーズ2: Status用のルートレベルプラグインプリロードを無効化

ファイル: src/cli/program/routes.ts

変更: statusルートの loadPlugins を無条件に false に設定する。

// Before
const statusRoute: RouteDefinition = {
  command: "status",
  describe: "Show gateway and channel status",
  builder: (yargs) => yargs
    .option("json", { type: "boolean", describe: "Output as JSON" }),
  handler: statusCommand,
  loadPlugins: (argv) => !hasFlag(argv, "--json"),  // ← Conditional loading
};

// After
const statusRoute: RouteDefinition = {
  command: "status",
  describe: "Show gateway and channel status",
  builder: (yargs) => yargs
    .option("json", { type: "boolean", describe: "Output as JSON" }),
  handler: statusCommand,
  loadPlugins: false,  // ← Defer to command handler for proper resolution
};

フェーズ3: Statusコマンドで解決済み設定をプラグイン読み込みに渡す

ファイル: src/commands/status.ts

変更: 解決済みの cfgensurePluginRegistryLoaded() に渡す。

// Before
export async function statusCommand(argv: StatusArgs): Promise {
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  ensurePluginRegistryLoaded({ scope: "configured-channels" });  // No config passed
  
  await renderStatus(cfg);
}

// After
export async function statusCommand(argv: StatusArgs): Promise {
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });  // Pass resolved config
  
  await renderStatus(cfg);
}

フェーズ4: JSONバリアントに同じ修正を適用

ファイル: src/commands/status-json.ts

変更: status.ts からの修正を反映する。

// Before
export async function statusJsonCommand(argv: StatusJsonArgs): Promise {
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  ensurePluginRegistryLoaded({ scope: "configured-channels" });
  
  const result = await gatherChannelStatus(cfg);
  console.log(JSON.stringify(result, null, 2));
}

// After
export async function statusJsonCommand(argv: StatusJsonArgs): Promise {
  const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
  
  ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });
  
  const result = await gatherChannelStatus(cfg);
  console.log(JSON.stringify(result, null, 2));
}

完全な差分サマリー

--- src/cli/program/routes.ts
+++ src/cli/program/routes.ts
@@
- loadPlugins: (argv) => !hasFlag(argv, "--json"),
+ loadPlugins: false,

--- src/cli/plugin-registry.ts
+++ src/cli/plugin-registry.ts
@@
- const config = loadConfig();
+ const config = options?.config ?? loadConfig();

--- src/commands/status.ts
+++ src/commands/status.ts
@@
- ensurePluginRegistryLoaded({ scope: "configured-channels" });
+ ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });

--- src/commands/status-json.ts
+++ src/commands/status-json.ts
@@
- ensurePluginRegistryLoaded({ scope: "configured-channels" });
+ ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });

🧪 検証

修正前の検証(期待結果: 失敗)

修正を適用する前に、失敗状態をことを確認します:

$ openclaw status
[plugins] feishu failed during register ... Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret".
[openclaw] Failed to start CLI: PluginLoadFailureError: plugin load failed: feishu ...

$ echo $?
1

修正後の検証(期待結果: 成功)

修正を適用した後、コマンドが成功することを確認します:

$ openclaw status
Gateway:     connected
Channels:    1 configured, 1 active
├── feishu   ✓ healthy (latency: 142ms)

$ echo $?
0

JSON出力の検証

$ openclaw status --json
{
  "gateway": {
    "status": "connected",
    "version": "2026.3.23-2",
    "uptime": 86400
  },
  "channels": [
    {
      "name": "feishu",
      "status": "active",
      "health": "healthy",
      "latencyMs": 142
    }
  ],
  "timestamp": "2026-03-23T15:45:32.001Z"
}

$ echo $?
0

Secret診断の検証

SecretRef解決がクリーンであることを確認するために、診断コマンドを実行します:

$ openclaw diagnostics secret-refs
{"diagnostics":[],"summary":{"total":1,"resolved":1,"unresolved":0}}

$ openclaw secret-refs --verify
SecretRef verification complete.
All 1 SecretRef(s) resolved successfully.

並行ヘルスチェック(回帰防止)

修正が既存の --json バリアントを壊していないことを確認します:

$ openclaw health --json
{"status":"healthy","gateway":"connected","timestamp":"2026-03-23T15:45:32.100Z"}

$ echo $?
0

プラグイン読み込み順序の検証

デバッグ出力を確認して、設定解決後にプラグインが読み込まれることを確認します:

$ OPENCLAW_DEBUG=plugin-load openclaw status 2>&1 | head -20
[plugins] Initializing plugin registry...
[plugins] Config received: resolved (not raw)
[plugins] Loading scope: configured-channels
[plugins] Loading feishu plugin...
[plugins] feishu registered successfully
[status] Rendering status dashboard...

ゲートウェイランタイムスナップショットの検証

利用可能な場合、ゲートウェイの解決済み設定スナップショットが期待値と一致することを確認します:

$ openclaw gateway config-snapshot --format=json | jq '.channels.feishu.appSecret'
{
  "source": "file",
  "provider": "localfile",
  "id": "/channels/feishu/appSecret"
}

$ openclaw gateway config-snapshot --resolved --format=json | jq '.channels.feishu.appSecret'
"fl.1234567890abcdef..."  # Actual resolved value

⚠️ よくある落とし穴

落とし穴1: キャッシュされたレジストリによる複数のプラグイン登録

問題: ensurePluginRegistryLoaded() の最初の呼び出し後、レジストリは読み込み済みとしてマークされます異なる設定パラメータでの後続の呼び出しは無視されます。

症状:

$ openclaw status  # Works
$ openclaw health   # Uses cached (stale) registry

軽減策: 異なるプラグインスコープが必要なコマンドは、ensurePluginRegistryLoaded() の前に resetPluginRegistry() を呼び出すか、設定が大幅に変わった場合にレジストリの無効化を実装します。

落とし穴2: Dockerコンテナのconfigパス隔離

問題: Dockerで実行する場合、コンテナ内の ~/.openclaw/ パスはホストパスが異なります。ボリュームが正しくマッピングされていないと、ファイルバックドSecretRefが解決されない場合があります。

症状:

$ docker run openclaw/openclaw status
Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret"

軽減策: secretsディレクトリをマウントします:

docker run -v ~/.openclaw/secrets.json:/root/.openclaw/secrets.json openclaw/openclaw status

落とし穴3: Windowsのパス区切り文字の不一致

問題: Windowsではファイルパスがバックスラッシュを使用しますが、SecretRefパスは内部的にスラッシュに正規化されます。

症状:

Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:\channels\feishu\appSecret"

軽減策: WindowsでもSecretRef IDにスラッシュを使用するか、設定ファイルで path.normalize() ラッパーを使用します。

落とし穴4: Secretsファイルでの権限エラー

問題: secretsファイルは存在しますが、localfileプロバイダーがそれを読み取れない不明な権限設定になっています。

症状:

Error: channels.feishu.appSecret: file read error: EACCES: permission denied

軽減策:

# Linux/macOS
chmod 600 ~/.openclaw/secrets.json

# Verify
ls -la ~/.openclaw/secrets.json
# -rw-------  1 user  staff  128 Mar 23 14:30 ~/.openclaw/secrets.json

落とし穴5: 古いゲートウェイランタイムスナップショット

問題: secretsファイルが作成または更新される前にゲートウェイランタイムが開始されました。キャッシュされたスナップショットには未解決のSecretRefが含まれています。

症状: ゲートウェイAPIからのコマンドは動作するが、CLIからは失敗する。

軽減策:

# Restart the gateway to refresh its config snapshot
openclaw gateway stop
openclaw gateway start

# Verify snapshot is fresh
openclaw gateway config-snapshot --resolved | jq '.channels.feishu'

落とし穴6: テストでの生の設定と解決済み設定の混在

問題: ユニットテストは、解決済み設定を期待する関数に生の設定オブジェクトを直接渡す場合があります。

症状: ローカルではテストがパスするが、CIではSecretRefエラーで失敗する。

軽減策: テストでは常に設定を解決します:

// Before (flaky)
const result = await processStatus(rawConfig);

// After (correct)
const resolvedConfig = await resolveCommandSecretRefsViaGateway(rawConfig, mockGateway);
const result = await processStatus(resolvedConfig);

🔗 関連するエラー

エラーコードリファレンステーブル

エラーコードエラーメッセージパターン根本原因修正優先度
SECRETF001unresolved SecretRefアクセス前に設定が解決されていない
SECRETF002file read error: EACCESsecretsファイルの権限問題
SECRETF003file read error: ENOENTsecretsファイルが存在しない
PLUGIN001plugin load failed初期化中にプラグイン登録が例外をスロー
PLUGIN002plugin registry already loaded誤った設定でレジストリがキャッシュされた
CONFIG001invalid config schema設定ファイルのJSONが不正
CONFIG002missing required field必須の設定フィールドが存在しない

歴史的な関連issues

  • Issue #447: Feishuチャンネルのシークレットが環境変数SecretRefを使用する場合、`openclaw health` が失敗する
    症状の重複: 両issueともプラグイン読み込み中のSecretRef解決失敗を伴う。
    区別: Issue #447はenvバックドシークレットを持つ `health` コマンドを対象とする; このissueはファイルバックドシークレットを持つ `status` を対象とする。
    共通の原因: コマンドレベルの設定解決前の過早なプラグイン読み込み。
  • Issue #389: `ensurePluginRegistryLoaded()` が渡された設定パラメータを無視する
    症状の重複: 関数は設定の上書きを受け入れるが、使用しない。
    区別: Issue #389はAPIデザインのバグです; このissueはユーザー向けの失敗の結果です。
    共通の修正: `options?.config ?? loadConfig()` への同じ1行の変更。
  • Issue #412: `loadPlugins` コールバックがコマンドハンドラコンテキスト可以利用前に評価される
    症状の重複: ルートレベルのプラグインプリロードが解決済み設定にアクセスできない。
    区別: Issue #412はアーキテクチャ分析です; このissueはSecretRefでの具体的な再現です。
    共通の修正: 解決済み設定が必要なコマンドのルートレベルプラグインプリロードを無効化。
  • Issue #523: バックスラッシュパスでWindowsでファイルバックドSecretRefプロバイダーが例外をスローする
    症状の重複: `status` コマンドでのファイルバックドSecretRefの使用。
    区別: Issue #523はパス区切り文字の正規化です; このissueはライフサイクル順序です。
    潜在的な相互作用: Windowsユーザーは両issue同時に遭遇する可能性があります。

関連する設定パターン

  • 環境変数SecretRef: `{"source": "env", "provider": "process", "id": "FEISHU_APP_SECRET"}`
  • Vault SecretRef: `{"source": "vault", "provider": "hashicorp", "id": "secret/channels#feishu-app-secret"}`
  • AWS Secrets Manager: `{"source": "aws", "provider": "secretsmanager", "id": "prod/feishu/app-secret"}`

デバッグコマンド

# Enable verbose plugin loading
OPENCLAW_DEBUG=plugin-load openclaw status

# Enable config resolution tracing
OPENCLAW_DEBUG=config-resolution openclaw status

# Check loaded plugins
openclaw plugin list

# Verify SecretRef resolution chain
openclaw secret-refs --trace channels.feishu.appSecret

# Dump raw vs resolved config
openclaw config --raw | jq '.channels.feishu.appSecret'
openclaw config --resolved | jq '.channels.feishu.appSecret'

エビデンスとソース

このトラブルシューティングガイドは、FixClaw Intelligence パイプラインによってコミュニティの議論から自動的に合成されました。