April 16, 2026 โ€ข Version: 2026.3.23-2

openclaw status Fails on File-Backed SecretRef Configs Due to Plugin Loading Re-Reading Unresolved Raw Config

When channel secrets are migrated to file-backed SecretRefs, the `openclaw status` command crashes during plugin registration because `ensurePluginRegistryLoaded()` re-reads unresolved raw config instead of using the already-resolved configuration from the gateway runtime.

๐Ÿ” Symptoms

Primary Error Manifestation

When executing openclaw status against a configuration where channel secrets use file-backed SecretRefs, the CLI crashes during plugin registration:

$ 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 ...

Successful Parallel Commands

Interestingly, related commands in the same environment succeed:

$ 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"}

Environment Context

The failure occurs under specific conditions:

  • OS: Linux `6.17.0-19-generic`
  • Node.js: `v22.22.1`
  • Install Method: Global package install under `~/.npm-global/lib/node_modules/openclaw`
  • OpenClaw Version: `2026.3.23-2`

SecretRef Configuration Pattern

The failing configuration uses the following SecretRef structure in ~/.openclaw/openclaw.json:

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

The actual secret value is stored in ~/.openclaw/secrets.json:

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

๐Ÿง  Root Cause

Architectural Overview

The failure stems from a config lifecycle mismatch between plugin loading and command-level SecretRef resolution. The CLI architecture has two competing config consumption paths:

  1. Command-level resolution: `status` command resolves SecretRefs via active gateway runtime snapshot
  2. Route-level plugin loading: Routes preload plugins using raw, unresolved config

Problem 1: Premature Plugin Preloading in Route Layer

In src/cli/program/routes.ts, the route configuration specifies:

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

This causes plugins to load for the status command before the status command handler executes. The plugin registration phase attempts to access channel credentials, which are still in their unresolved SecretRef form.

Problem 2: Plugin Registry Always Reads Raw Config

In src/cli/plugin-registry.ts, the ensurePluginRegistryLoaded() function unconditionally calls 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;
}

The function signature accepts an options parameter but does not utilize it for config override:

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

Problem 3: Status Command Resolution Happens Too Late

The status command in src/commands/status.ts correctly resolves SecretRefs via gateway:

// 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);
}

Execution Timeline Comparison

Phaseopenclaw health --jsonopenclaw status
Route plugin preloadSkipped (has --json)Triggers crash
Command handler execResolves configResolves config
Plugin registrationUses resolved configUses unresolved config

Why --json Flag Works

The --json flag bypasses plugin preloading because the route configuration conditionally skips plugin loading:

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

With --json, plugin loading is deferred until after the command handler resolves the config, allowing ensurePluginRegistryLoaded() to receive the correct configuration context.

๐Ÿ› ๏ธ Step-by-Step Fix

Phase 1: Modify Plugin Registry to Accept Config Override

File: src/cli/plugin-registry.ts

Change: Utilize the existing but unused config option parameter.

// 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;
}

Phase 2: Disable Route-Level Plugin Preloading for Status

File: src/cli/program/routes.ts

Change: Set loadPlugins to false unconditionally for the status route.

// 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
};

Phase 3: Pass Resolved Config to Plugin Loading in Status Command

File: src/commands/status.ts

Change: Pass the resolved cfg to ensurePluginRegistryLoaded().

// 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);
}

Phase 4: Apply Same Fix to JSON Variant

File: src/commands/status-json.ts

Change: Mirror the fix from 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));
}

Complete Diff Summary

--- 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 });

๐Ÿงช Verification

Pre-Fix Verification (Expected: Failure)

Before applying the fix, confirm the failure state:

$ 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

Post-Fix Verification (Expected: Success)

After applying the fix, verify the command succeeds:

$ openclaw status
Gateway:     connected
Channels:    1 configured, 1 active
โ”œโ”€โ”€ feishu   โœ“ healthy (latency: 142ms)

$ echo $?
0

JSON Output Verification

$ 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 Diagnostics Verification

Execute the diagnostic command to confirm SecretRef resolution is clean:

$ 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.

Parallel Health Check (Regression Prevention)

Verify that the fix does not break the existing --json variant:

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

$ echo $?
0

Plugin Load Order Verification

Confirm plugins load after config resolution by checking debug output:

$ 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...

Gateway Runtime Snapshot Verification

If available, verify the gateway’s resolved config snapshot matches expectations:

$ 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

โš ๏ธ Common Pitfalls

Pitfall 1: Multiple Plugin Registrations with Cached Registry

Problem: After the first call to ensurePluginRegistryLoaded(), the registry is marked as loaded. Subsequent calls with different config parameters will be ignored.

Symptom:

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

Mitigation: Ensure commands that require different plugin scopes call resetPluginRegistry() before ensurePluginRegistryLoaded(), or implement registry invalidation when config changes significantly.

Pitfall 2: Docker Container Config Path Isolation

Problem: When running in Docker, ~/.openclaw/ paths inside the container differ from host paths. File-backed SecretRefs may not resolve if volumes are not properly mapped.

Symptom:

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

Mitigation: Mount the secrets directory:

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

Pitfall 3: Windows Path Separator Mismatch

Problem: On Windows, file paths use backslashes, but SecretRef paths are normalized to forward slashes internally.

Symptom:

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

Mitigation: Use forward slashes in SecretRef IDs even on Windows, or use the path.normalize() wrapper in config files.

Pitfall 4: Permission Denied on Secrets File

Problem: The secrets file exists but has incorrect permissions, preventing the localfile provider from reading it.

Symptom:

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

Mitigation:

# 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

Pitfall 5: Stale Gateway Runtime Snapshot

Problem: The gateway runtime was started before the secrets file was created or updated. The cached snapshot contains unresolved SecretRefs.

Symptom: Commands work from the gateway API but fail from CLI.

Mitigation:

# 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'

Pitfall 6: Mixing Raw and Resolved Config in Tests

Problem: Unit tests may pass a raw config object directly to functions expecting resolved config.

Symptom: Tests pass locally but fail in CI with SecretRef errors.

Mitigation: Always resolve config in tests:

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

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

Error Code Reference Table

Error CodeError Message PatternRoot CauseFix Priority
SECRETF001unresolved SecretRefConfig not resolved before accessHigh
SECRETF002file read error: EACCESPermission issue on secrets fileMedium
SECRETF003file read error: ENOENTSecrets file does not existMedium
PLUGIN001plugin load failedPlugin registration threw during initHigh
PLUGIN002plugin registry already loadedRegistry cached with wrong configLow
CONFIG001invalid config schemaConfig file malformed JSONLow
CONFIG002missing required fieldRequired config field absentLow
  • Issue #447: `openclaw health` fails when Feishu channel secret uses environment variable SecretRef
    Symptom overlap: Both issues involve SecretRef resolution failure during plugin loading.
    Distinction: Issue #447 targets the `health` command with env-backed secrets; this issue targets `status` with file-backed secrets.
    Shared root: Premature plugin loading before command-level config resolution.
  • Issue #389: `ensurePluginRegistryLoaded()` ignores passed config parameter
    Symptom overlap: Function accepts config override but does not use it.
    Distinction: Issue #389 is the API design bug; this issue is the user-facing failure consequence.
    Shared fix: The same single-line change to `options?.config ?? loadConfig()`.
  • Issue #412: `loadPlugins` callback evaluates before command handler context is available
    Symptom overlap: Route-level plugin preload cannot access resolved config.
    Distinction: Issue #412 is the architectural analysis; this issue is the concrete reproduction with SecretRefs.
    Shared fix: Disable route-level plugin preload for commands that need resolved config.
  • Issue #523: File-backed SecretRef provider throws on Windows with backslash paths
    Symptom overlap: File-backed SecretRef usage in `status` command.
    Distinction: Issue #523 is path separator normalization; this issue is lifecycle ordering.
    Potential interaction: Users on Windows may encounter both issues simultaneously.
  • Environment Variable 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"}`

Debugging Commands

# 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'

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.