#19728: feat: add apiKeyFile support for model providers
app: macos
agents
size: S
Cluster:
Model Authentication Enhancements
## 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
#21216: feat(models): add apiKeyHelper for dynamic API key resolution
by chrisvanbuskirk · 2026-02-19
76.3%
#14836: fix: strip apiKey from models.json to prevent credential exposure
by pahud · 2026-02-12
70.3%
#16766: fix(model): apply provider baseUrl/headers override to registry-fou...
by dzianisv · 2026-02-15
69.1%
#15756: [Security]: strip provider apiKey from models.json before prompt se...
by SecBear · 2026-02-13
69.0%
#16290: fix: add field-level validation for custom LLM provider config
by superlowburn · 2026-02-14
66.7%
#6683: feat(config): add supportsStrictMode compat option for model defini...
by long-pham · 2026-02-01
66.4%
#16663: feat: GCP Secret Manager integration for external secrets management
by amor71 · 2026-02-15
66.2%
#22069: fix(agents): add provider-specific hints for local model auth errors
by pierreeurope · 2026-02-20
66.2%
#14475: feat: Add native Azure OpenAI support
by dzianisv · 2026-02-12
64.6%
#18670: feat: add first-class Claude Code CLI auth path + CLI model UX hard...
by SmithLabsLLC · 2026-02-16
64.1%