#15756: [Security]: strip provider apiKey from models.json before prompt serialization
agents
stale
size: S
Cluster:
Model Authentication Enhancements
## Summary
Strip provider `apiKey` values from `models.json` before disk write.
The agent SDK reads this file and may expose credentials in prompt context.
Auth resolution is unaffected — `model-auth.ts` injects keys via `authStorage.setRuntimeApiKey()` independently.
Fixes #11202 · Implements Layer 1 of #11829 · Related: #14411
## Security Impact
**Risk:** Literal API keys from auth profiles leak into `~/.openclaw/agents/<id>/models.json` via `resolveApiKeyFromProfiles()` at `models-config.providers.ts:278`. The pi-coding-agent SDK reads this file, making credentials accessible to agent filesystem tools and potentially
serializable into LLM prompt context. Every provider sees every other provider's keys, every turn. Confirmed in v2026.2.3-1 through v2026.2.6-3 (#11202).
**Repro:**
1. Configure a provider with an auth profile (not env var)
2. Start an agent session
3. `cat ~/.openclaw/agents/default/models.json | grep apiKey`
4. Observe: literal secret visible in plaintext
**Fix:** Add `stripProviderSecrets()` at the serialization boundary in `ensureOpenClawModelsJson()` (`models-config.ts:152`). Replace credential fields with `"REDACTED_BY_OPENCLAW"` placeholder — SDK still considers custom models "available" (non-empty apiKey).
**Code path traced:**
- `models-config.providers.ts:278-283` — `resolveApiKeyFromProfiles()` returns literal secrets
- `models-config.ts:148-155` — normalization then stripping before `JSON.stringify`
- `model-auth.ts:135-233` — auth resolution reads from runtime config, env, profiles (NOT models.json)
- `run.ts:343-345` — `authStorage.setRuntimeApiKey()` injects real auth before session creation
I searched the codebase for existing functionality — `ModelCatalogEntry` type at `model-catalog.ts:5-12` already excludes `apiKey`, confirming the agent doesn't need it.
**Known remaining vectors:**
- `session-status-tool.ts:56-64` — `formatApiKeySnippet()` shows truncated key heads/tails to the agent via `/status` tool output. Lower severity (partial, not full key), separate scope.
- `models.json` on disk — agent has filesystem tools and could read other config files (`openclaw.config.json`, auth-profiles) that may contain credentials. Process-level isolation (#12839) addresses this vector.
- Upstream SDK context — `@mariozechner/pi-coding-agent` may include model metadata in internal agent context beyond the system prompt. Layer 3 of #11829 tracks upstream filtering.
## Changes
- **`src/agents/models-config.ts`** — Add `REDACTED_PLACEHOLDER` constant, `stripProviderSecrets()` helper, call before `JSON.stringify` at serialization boundary
- **`src/agents/models-config.strip-secrets.test.ts`** *(new)* — 3 tests: single provider stripping, multi-provider stripping, no-key graceful handling
- **`src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts`** — apiKey assertion updated to `REDACTED_PLACEHOLDER`
- **`src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts`** — 2 apiKey assertions updated to `REDACTED_PLACEHOLDER`
## Validation / Test Plan
**Automated:**
- `models.json` contains `"REDACTED_BY_OPENCLAW"` for all apiKey fields
- No `sk-` patterns in serialized output
- Agent can still make API calls (auth resolves via `model-auth.ts`, not `models.json`)
- `pnpm check` passes
- `pnpm build` passes
- `pnpm test` passes (628 files, 4382 tests — all green)
- All `model-auth`, `model-catalog`, remaining `models-config` tests unchanged and passing
**Manual:**
1. Configure 2+ providers with different auth methods
2. Start agent, verify API calls succeed for all providers
3. Inspect `~/.openclaw/agents/default/models.json` — all apiKeys show `REDACTED_BY_OPENCLAW`
4. Verify `/models` list and model switching still work
## Sign-Off
- **Models used:** Claude Opus 4.6 (architecture analysis, implementation via Claude Code)
- **Submitter effort:** Manual security research, code path tracing, PR strategy. AI-assisted implementation.
- **Agent notes:** Auth pipeline independence verified by tracing `model-auth.ts` → `run.ts:345` → `authStorage.setRuntimeApiKey()`. Confirmed `models.json` is not read back for API calls.
lobster-biscuit
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR prevents provider credentials from being persisted into `~/.openclaw/agents/<id>/models.json` by replacing any `providers[...].apiKey` values with the constant `REDACTED_BY_OPENCLAW` at the serialization boundary (`src/agents/models-config.ts`).
The rest of the agent runtime continues to resolve real credentials via `model-auth` and inject them into the embedded runner’s `AuthStorage` using `authStorage.setRuntimeApiKey(...)` (see `src/agents/pi-embedded-runner/run.ts`), so `models.json` remains a model/metadata registry file rather than an auth source. Tests were updated/added to assert that the redacted placeholder is written and that literals are not present on disk.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk.
- The change is narrowly scoped to the `models.json` write path and replaces secrets with a placeholder while leaving runtime auth resolution unchanged (runtime keys are injected via `authStorage.setRuntimeApiKey(...)`). The added/updated tests cover the intended redaction behavior and reduce regression risk.
- No files require special attention
<sub>Last reviewed commit: ba0e976</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#14836: fix: strip apiKey from models.json to prevent credential exposure
by pahud · 2026-02-12
87.0%
#9163: Fix: Save Anthropic setup token to config file
by vishaltandale00 · 2026-02-04
80.7%
#23286: fix: use configured model in llm-slug-generator instead of hardcoded …
by wsman · 2026-02-22
80.2%
#6673: fix: preserve allowAny flag in createModelSelectionState for custom...
by tenor0 · 2026-02-01
79.9%
#12059: feat(agents): Add Azure AI Foundry credential support
by lisanyambere · 2026-02-08
79.8%
#11198: fix(models): strip @profile suffix from model selection
by mcaxtr · 2026-02-07
79.6%
#9739: #9291 fix(models): preserve existing models in models.json when mer...
by ximzzzzz · 2026-02-05
79.5%
#9583: fix(models): allow models in agents.defaults.models even if not in ...
by hotzen100 · 2026-02-05
79.5%
#7570: fix: allow models from providers with auth profiles configured
by DonSqualo · 2026-02-03
79.4%
#15632: fix: use provider-qualified key in MODEL_CACHE for context window l...
by linwebs · 2026-02-13
79.0%