Skip to content

bug: config loaded and resolved twice per CLI invocation #163

@JoshMock

Description

@JoshMock

Description

For most CLI invocations, the configuration file is loaded, expressions are resolved, and schemas are validated twice:

  1. Early load (line ~97): loadConfig({}) is called unconditionally (for all commands except version) to populate the config store so hideBlockedCommands can filter the help text.
  2. preAction hook (line ~31): loadConfig() is called again with the same options right before the command handler runs.

Both calls perform the full pipeline: file discovery, YAML parsing, structural validation, expression resolution, and context schema validation. The setResolvedConfig call in the early load is overwritten by the preAction hook, so the first load's result is discarded (except for its use by hideBlockedCommands).

For configs that use slow expression resolvers (e.g., $(keychain:...), $(secret_service:...), $(credential_manager:...), $(cmd:...)), this doubles the number of shell-outs per invocation, adding noticeable latency.

Steps to Reproduce

  1. Create a .elasticrc.yml that uses a slow resolver, e.g.:
    current_context: default
    contexts:
      default:
        elasticsearch:
          url: https://localhost:9200
          auth:
            api_key: $(keychain:elastic-cli/api-key)
  2. Run any command and observe that the keychain lookup happens twice:
    time elastic es info
  3. Alternatively, use $(cmd:sleep 1) as a synthetic slow resolver to make the double-load obvious:
    current_context: default
    contexts:
      default:
        elasticsearch:
          url: https://localhost:9200
          auth:
            api_key: $(cmd:sleep 1 && echo my-key)
    elastic es info will take ~2 seconds instead of ~1.

Expected behavior

The config should be loaded and resolved once per invocation. The result should be reused by both hideBlockedCommands and the preAction hook.

Root Cause

In src/cli.ts, the early load on line ~97 does not pass --config-file or --use-context options (it calls loadConfig({}) with no arguments), while the preAction hook on line ~31 passes the CLI flags. Since they may resolve different configs, the early load's result cannot simply be reused by the preAction hook as-is.

However, in practice the early load's result is always discarded by the preAction hook's setResolvedConfig call, making the early load purely overhead except for the hideBlockedCommands side effect.

Recommended Solution

Cache the early load result and reuse it in the preAction hook when the CLI flags match (i.e., no --config-file or --use-context overrides). For example:

let earlyConfig: ResolvedConfig | undefined

if (firstArg !== 'version') {
  const earlyResult = await loadConfig({})
  if (earlyResult.ok) {
    earlyConfig = earlyResult.value
    setResolvedConfig(earlyResult.value)
    hideBlockedCommands(program, earlyResult.value.commands)
  }
}

program.hook('preAction', async (thisCommand, actionCommand) => {
  if (actionCommand.name() === 'version') return
  if (actionCommand.parent?.name() === 'docs') return

  const { configFile: configPath, useContext: contextName } = thisCommand.opts()

  // Reuse the early load if no overrides were specified
  if (configPath == null && contextName == null && earlyConfig != null) {
    setResolvedConfig(earlyConfig)
    return
  }

  const result = await loadConfig({
    ...(configPath != null && { configPath }),
    ...(contextName != null && { contextName })
  })
  if (result.ok) {
    setResolvedConfig(result.value)
  } else {
    process.stderr.write(`Error: ${result.error.message}\n`)
    process.exit(1)
  }
})

This eliminates the second load for the common case (no CLI overrides) while preserving correct behavior when --config-file or --use-context are specified.

Related

Parent issue: #71

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions