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:33import { 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

  1. ✅ Replace try { loadConfig(); } catch { fallback; } with direct import from src/config.ts.
  2. ✅ Preserve apiKey, model, timeoutMs injection seams via CompletionOptions.
  3. ✅ Preserve AnthropicConfigError semantics — unchanged.
  4. ✅ Singleton handles call-time validation — config.ANTHROPIC_API_KEY is string | undefined.
  5. ✅ Add tests for default-model resolution + injected-override precedence.
  6. ✅ All gates pass: npm run build && npm run lint && npm test.

Out of scope

  • Config type or src/config.ts schema changes.
  • claude.ts retry 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.ts precedents — those don’t use config at all.

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.