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:
- Command-level resolution: `status` command resolves SecretRefs via active gateway runtime snapshot
- 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
| Phase | openclaw health --json | openclaw status |
|---|---|---|
| Route plugin preload | Skipped (has --json) | Triggers crash |
| Command handler exec | Resolves config | Resolves config |
| Plugin registration | Uses resolved config | Uses 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);
๐ Related Errors
Error Code Reference Table
| Error Code | Error Message Pattern | Root Cause | Fix Priority |
|---|---|---|---|
SECRETF001 | unresolved SecretRef | Config not resolved before access | High |
SECRETF002 | file read error: EACCES | Permission issue on secrets file | Medium |
SECRETF003 | file read error: ENOENT | Secrets file does not exist | Medium |
PLUGIN001 | plugin load failed | Plugin registration threw during init | High |
PLUGIN002 | plugin registry already loaded | Registry cached with wrong config | Low |
CONFIG001 | invalid config schema | Config file malformed JSON | Low |
CONFIG002 | missing required field | Required config field absent | Low |
Historically Related Issues
- 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.
Related Configuration Patterns
- 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'