Audit — fix-claude-config-cache
Scope
Code-review Medium Finding #11: src/domains/integrations/claude.ts calls
loadConfig(process.env) on every API call inside a try/catch with a silent
fallback to the literal string 'claude-sonnet-4-5'. Replace with the canonical
frozen-singleton import (import { config } from '../../config.js') used
everywhere else in the codebase.
Surface inventory
Target file
| Path | LOC | Role |
|---|---|---|
src/domains/integrations/claude.ts |
405 | ν Claude API wrappers (P0.9.2) |
Drift sites (the only two places that will change)
| Site | Lines | Pattern |
|---|---|---|
createCompletion |
344–353 | let defaultModel = 'claude-sonnet-4-5'; try { const cfg = loadConfig(process.env); defaultModel = cfg.COLIBRI_ANTHROPIC_MODEL; } catch {} const model = options.model ?? defaultModel; |
createCompletionWithTools |
385–393 | Identical pattern, duplicated |
Import line
src/domains/integrations/claude.ts:33 — import { loadConfig } from '../../config.js';
After the change, this becomes import { config } from '../../config.js';.
Reference inventory — canonical singleton pattern
config is exported as the eagerly-frozen singleton at src/config.ts:154:
export const config: Config = loadConfig();
Every other consumer in the codebase uses this pattern:
| Site | Line | Import |
|---|---|---|
src/server.ts |
46 | import { config } from './config.js'; |
src/startup.ts |
42 | import { config } from './config.js'; |
src/__tests__/startup.test.ts |
32 | import { config } from '../config.js'; |
loadConfig is also imported by src/__tests__/config.test.ts:20 and
src/__tests__/domains/integrations/notifications.test.ts:28 — both are test
files that intentionally re-validate the schema with custom env objects.
claude.ts is the only src/domains/ file that uses loadConfig directly,
and the only production-code loadConfig consumer outside the singleton
itself.
Schema invariants (already satisfied — no schema changes needed)
src/config.ts already defines (lines 84, 88–94):
COLIBRI_ANTHROPIC_MODEL: z.string().default('claude-sonnet-4-5'),
COLIBRI_ANTHROPIC_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
Both have defaults baked in. The frozen singleton always carries a non-empty
COLIBRI_ANTHROPIC_MODEL. The hardcoded 'claude-sonnet-4-5' literal in
claude.ts is therefore redundant — the schema default is the single source
of truth.
Test surface
Existing test file
src/__tests__/domains/integrations/claude.test.ts — 894 LOC, 9 describe
blocks, ~30 tests. All tests inject model: FAKE_MODEL (‘claude-test-model’)
explicitly via CompletionOptions. No test depends on the silent-fallback
default model resolution.
Tests that touch the config schema (lines 736–791)
A describe('config — ANTHROPIC_API_KEY env schema') block exercises
loadConfig directly with custom env objects — these tests are not
testing claude.ts and remain untouched.
Existing fallback-default coverage gap
No test currently verifies that createCompletion/createCompletionWithTools
default to config.COLIBRI_ANTHROPIC_MODEL when no model override is
passed. This gap exists today and will be filled by the new tests.
Risk inventory
| Risk | Severity | Mitigation |
|---|---|---|
| Singleton evaluated at module load — what if env malformed? | Low | Already true for the rest of the codebase. src/config.ts is the first import boot path and validation already runs at startup. The drift in claude.ts was the anomaly, not the rule. |
| Test injection seam regression | Low | The seam is options.model. It already exists. The change only swaps the source of the default. All 30+ existing tests pass model: FAKE_MODEL explicitly. |
process.env mutation between calls is no longer reflected |
None | The frozen singleton is immutable post-load. This was never a documented contract — and loadConfig(process.env) was being re-invoked on every call only because of the silent-fallback try/catch, not because anything depended on env mutation. |
| Silent-fallback was load-bearing for a startup test | None — verified | src/__tests__/startup.test.ts imports config directly. No test deliberately exercises the silent-fallback path with a malformed env. |
Acceptance criteria from dispatch packet
- ✅ Replace
try { loadConfig(); } catch { fallback; }with direct import fromsrc/config.ts. - ✅ Preserve
apiKey,model,timeoutMsinjection seams viaCompletionOptions. - ✅ Preserve
AnthropicConfigErrorsemantics — unchanged. - ✅ Singleton handles call-time validation —
config.ANTHROPIC_API_KEYisstring | undefined. - ✅ Add tests for default-model resolution + injected-override precedence.
- ✅ All gates pass:
npm run build && npm run lint && npm test.
Out of scope
Configtype orsrc/config.tsschema changes.claude.tsretry logic, token accounting, response parsing, error classes, tool-use handling.- Public API of
claude.ts— all function signatures are preserved. - The
notifications.ts,mcp-bridge.tsprecedents — those don’t use config at all.