Skip to content

design claude code config

sinrimin edited this page Oct 20, 2026 · 1 revision

Claude Code dedicated config panel (#60)

Issue #60. The runtime-config form becomes target-aware: claude-code grows a dedicated panel modeled on the open-source cc-switch project's claude form. The guiding rule: each Agent target has its own real configuration vocabulary — instead of bridging the #51 provider-model registry onto target-native slots, the panel speaks claude-code's own config surface. From the provider-config flow only the CONNECTION (provider → baseURL + credential) and the model ids carry in; the provider row keeps just that, and every model-shaped or behavior-shaped control lives in the panel, one surface per knob (no duplicate model fields beside the provider row).

Ground truth was verified against three agreeing sources: the cc-switch source, the installed claude-code 2.1.205 binary (strings), and the official env-vars / model-config docs.

Wire

A typed, additive claudeCode object on RuntimeConfigSpec (packages/shared/src/schemas/runtime-config.ts), mirroring the modelEntries precedent: old daemons' zod parse strips the unknown field, so the wire stays compatible. NOT the loose extra record. Core mirrors the shape as ClaudeCodeConfigData on RuntimeConfigSpecData (packages/core/src/domain/runtime-config.ts, kept in sync by hand like every other spec field). The service carries it through the upsert row and the GET view verbatim; the client bundle (/api/client/runtime-config) emits the row spec as-is, so a new daemon receives the knobs with zero route changes.

claudeCode: {
  authField?: 'ANTHROPIC_AUTH_TOKEN' | 'ANTHROPIC_API_KEY',
  switches?: { attributionHidden?, teammates?, toolSearch?, maxEffort?,
               autoUpgradeDisabled?, artifactsDisabled? },   // booleans, true-only on the wire
  tiers?: [{ tier: 'sonnet'|'opus'|'fable'|'haiku',           // model may carry [1m]
             model, displayName?, capabilities? }],
  subagentModel?,                                            // may carry [1m]
  maxOutputTokens?,        // int 1..128000
  maxContextTokens?,       // positive int
  extraBody?,              // JSON object record
}

An all-default draft serializes to undefined — an untouched row keeps its pre-#60 shape byte-for-byte.

The [1m] marker

[1m] appended to a model id (alias or full name) is claude-code's 1M context switch: the id is sent marker-stripped with the context-1m beta, and for unrecognized gateway ids claude-code ASSUMES a 1M window (in which case CLAUDE_CODE_MAX_CONTEXT_TOKENS does not apply — the UI warns about the conflict). The marker lives INSIDE the model strings end to end (cc-switch's representation):

  • shared strip/set/has helpers (hasClaudeOneMarker etc.) are the single implementation; web checkboxes are derived state over them, never a parallel boolean.
  • the web form's model STATE never carries the marker: prefill strips it into the panel's oneM checkbox, apply re-appends it onto spec.model.
  • the daemon writes markers wherever claude-code reads a model slot (settings model, tier envs, subagent env) and strips them from availableModels entries — the allowlist matches plain ids.

Daemon writer (applyClaudeConfig)

All knobs land in ~/.claude/settings.json with the established merge-preserving/revert semantics — a knob not configured REMOVES the key we manage; user keys are never touched; re-apply is idempotent; 0600.

Knob Written key(s)
auth slot env.ANTHROPIC_AUTH_TOKEN (default) or env.ANTHROPIC_API_KEY; the OTHER one is removed (claude-code warns when both are set)
tier pin env.ANTHROPIC_DEFAULT_{TIER}_MODEL (+ _NAME, _SUPPORTED_CAPABILITIES comma-joined when present); unpinned tiers remove all three; the legacy ANTHROPIC_SMALL_FAST_MODEL is retired on sight
subagent env.CLAUDE_CODE_SUBAGENT_MODEL
output cap env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
context window env.CLAUDE_CODE_MAX_CONTEXT_TOKENS
extra body env.CLAUDE_CODE_EXTRA_BODY (stringified JSON object)
hide attribution settings attribution = {commit:'', pr:'', sessionUrl:false} (the settings-object successor of the deprecated includeCoAuthoredBy) — BASE_URL-style ownership while a spec exists
teammates env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
tool search env.ENABLE_TOOL_SEARCH=true — deliberately UNPREFIXED, that is its documented spelling
max effort env.CLAUDE_CODE_EFFORT_LEVEL=max (top of low/medium/high/xhigh/max)
disable auto-update env.DISABLE_AUTOUPDATER=1 — unprefixed on purpose
disable artifact env.CLAUDE_CODE_DISABLE_ARTIFACT=1

availableModels = marker-stripped unique([model, ...models, ...tier pins, subagent]) — the allowlist is the LOCK (a tier pin the allowlist rejects would dead-letter the alias), so every id we manage joins it.

Web panel (claude-code-panel.tsx)

Four sections — 模型 / 别名档位映射 / 行为 / 请求:

  • 模型: default model (registry select when the picked provider has chat models, free text otherwise; a stored pre-registry id stays visible and submittable) + the 1M checkbox; the 可选附加模型 allowlist rows (add / promote-to-default / remove — the generic form's rows, moved in); subagent model + 1M.
  • 别名档位映射: sonnet/opus/fable/haiku rows — model (free text with registry datalist suggestions), 显示名 (_NAME), 能力声明 (_SUPPORTED_CAPABILITIES, a kit TagCombobox suggesting effort/thinking but accepting custom tokens — the docs' vocabulary is open), per-row 1M checkbox, and a 模型应用到所有档 button.
  • 行为: the six managed switches above, plus the #49 default permission mode row (machine-scoped save via updateMachine, rendered in place as a slot — it is claude-code-only by design, so the panel is its home now).
  • 请求: auth slot, 输出上限 (1..128000), 上下文窗口 (with the [1m]-conflict warning), 请求体扩展 (JSON textarea; an invalid object blocks the apply button).

Draft shape + prefill/serialize/validate live in the panel module (CcDraft, ccDraftFromSpec, serializeCcDraft, ccIssue); the form's apply button stays disabled while ccIssue reports a problem. i18n keys under machineDetail.cc* (en/zh); wire/protocol values (env names, [1m], tier names) stay English in both locales.

Deliberately NOT built

  • cc-switch's 通用配置 (common-config) overlay — an arbitrary-keys JSON layer merged on top of every switch. We only write values our panel explicitly configures; everything else in the native file stays untouched.
  • ANTHROPIC_CUSTOM_MODEL_OPTION — availableModels + tier pins are strictly more capable for our multi-model registry story.
  • modelOverrides / modelPicker — claude-family-name anchored (wrong direction for gateway ids) / redundant with the allowlist.
  • deniedModels, enforceAvailableModels — managed-settings-only keys.
  • Bridging #51 modelEntries metadata (displayName/contextWindow/vision) into claude-code — explicitly not carried over; the tier _NAME pins are claude-code's own display surface.
  • Per-target panels for the other targets — follow-ups in the same pattern.

Boundaries

  • The env knobs are per-install globals: in-session model switches (the W13 dropdown) do not re-write tier mappings; switching the default model in-session does not carry [1m] (the picker entry is plain).
  • Older claude-code versions harmlessly ignore the newer env keys; the attribution object and fable tier are 2.1-era surfaces.
  • Tests: packages/cli/test/runtime-config.test.ts (knob write, marker flow, allowlist union, idempotency, knob-less revert) and packages/shared/test/runtime-config.test.ts (schema bounds, marker helper round-trips).

Clone this wiki locally