← Back to PRs

#19728: feat: add apiKeyFile support for model providers

by paperMoose open 2026-02-18 03:57 View on GitHub →
app: macos agents size: S
## Summary Add `apiKeyFile` as a new field on model provider config, allowing API keys to be read from a file path at runtime instead of being stored inline. This follows the existing `*File` pattern already used by channel providers (Telegram `tokenFile`, IRC `passwordFile`, LINE `tokenFile`/`secretFile`, Google Chat `serviceAccountFile`) and extends it to model providers, closing the gap identified in #9627 and #14411. ### Problem When `apiKey` is set as a literal string in `models.json`, config-write operations serialize it back to disk in plaintext. The `restoreEnvVarRefs()` fix (#11560) only works for `${VAR}` references, not literal keys. `apiKeyFile` is the structural fix: the config stores a path, and the secret never touches the config file. ### Changes - **`zod-schema.core.ts`**, add `apiKeyFile: z.string().optional()` to `ModelProviderSchema` (not marked sensitive, it's a path) - **`types.models.ts`**, add `apiKeyFile?: string` to `ModelProviderConfig` - **`model-auth.ts`**, `getCustomProviderApiKey()` reads from file when `apiKeyFile` is set (file takes precedence over `apiKey`), uses sync `readFileSync` matching existing channel patterns, logs warnings on missing/unreadable files - **`schema.hints.ts`**, whitelist `apiKeyFile` in `SENSITIVE_KEY_WHITELIST_SUFFIXES` to prevent false-positive from the `/api.?key/i` sensitive-field regex - **`models-config.providers.ts`**, set `"__apiKeyFile__"` placeholder so ModelRegistry registers models, skip `normalizeApiKeyConfig()` when `apiKeyFile` is set - **`schema.hints.test.ts`**, add whitelist test case for `apiKeyFile` - **New: `model-auth.api-key-file.test.ts`**, 6 tests covering file read, precedence over apiKey, placeholder resolution, missing file, empty file, fallback to apiKey - **New: `models-config.apiKeyFile-placeholder.e2e.test.ts`**, verifies models.json gets the placeholder (not the real secret) when apiKeyFile is configured ### Design decisions - **File takes precedence over apiKey**, matches Telegram `tokenFile` behavior - **Sync file read**, matches all existing `*File` implementations, called during startup - **Re-read on every auth call**, supports secret rotation without restart - **Path normalization**, `normalize-paths.ts` `PATH_KEY_RE` already matches the `file` suffix, so `~/` expansion works automatically - **Not marked sensitive in Zod**, the value is a file path, not a secret - **Placeholder in models.json**, `"__apiKeyFile__"` satisfies ModelRegistry without leaking the real key to disk ### Usage ```json { "models": { "providers": { "openai": { "apiKeyFile": "/run/secrets/openai-api-key", "baseUrl": "https://api.openai.com/v1", "models": [...] } } } } ``` Discussion: https://github.com/openclaw/openclaw/discussions/19719 Refs: #9627, #14411 ## Test plan - [x] 6 unit tests pass (`model-auth.api-key-file.test.ts`) - [x] 1 e2e test verifies placeholder in models.json (`models-config.apiKeyFile-placeholder.e2e.test.ts`) - [x] Existing `schema.hints.test.ts` passes with new whitelist entry - [x] `redact-snapshot.test.ts` passes (no regressions in sensitive field detection) - [x] End-to-end smoke test: OpenClaw loads config, resolves key from file correctly - [x] oxlint clean on all changed files - [x] oxfmt check passes 🤖 Generated with [Claude Code](https://claude.com/claude-code)

Most Similar PRs