Repository navigation
design claude code config
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.
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.
[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 (
hasClaudeOneMarkeretc.) 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
oneMcheckbox, apply re-appends it ontospec.model. - the daemon writes markers wherever claude-code reads a model slot
(settings
model, tier envs, subagent env) and strips them fromavailableModelsentries — the allowlist matches plain ids.
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.
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 kitTagComboboxsuggestingeffort/thinkingbut 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.
- 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
modelEntriesmetadata (displayName/contextWindow/vision) into claude-code — explicitly not carried over; the tier_NAMEpins are claude-code's own display surface. - Per-target panels for the other targets — follow-ups in the same pattern.
- 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
attributionobject 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) andpackages/shared/test/runtime-config.test.ts(schema bounds, marker helper round-trips).