From 62168905ff6980a13deb84e7bd2b25a90659ff66 Mon Sep 17 00:00:00 2001 From: jellologic <31935831+jellologic@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:54:07 -0400 Subject: [PATCH 1/3] feat(web): browse every environment variable Claude Code references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 495 of them, extracted from the shipped binary (2.1.217): 60 documented, 338 undocumented, 97 internal. Searchable by name OR by description, so "output tokens" finds CLAUDE_CODE_MAX_OUTPUT_TOKENS. THE CLASSIFICATION IS THE FEATURE, not a caveat attached to it. Reading strings out of a binary yields names and nothing else — no meaning, no defaults, no evidence a name is still wired to anything. Presenting 495 strings as if they were equally supported knobs would invite someone to set an unreleased feature codename in a profile and wonder why nothing happened, or why it stopped working after an agent update. So every entry declares how much is known: documented described by hand from Anthropic's docs, `claude --help`, or swisscode's own adapter. Safe to act on. undocumented the name is real; the meaning is not. Ships with NO description on purpose — a plausible guess is worse than a blank, because someone acts on it. internal test hooks, profiling, third-party cloud auth, and two-word feature codenames (ALDER_WICKET, BISON_CAIRN). Listed for completeness; not knobs. Descriptions are hand-written and never extracted. `test/adapters/ claude-env-catalog.test.ts` enforces that a non-documented entry cannot carry one, that every "internal" verdict states its reason, and that every variable the adapter itself sets appears documented and flagged — a catalog that omitted those would invite someone to hand-set a variable the launcher already owns. Those tests immediately earned their keep: they caught the extractor running its shape heuristics BEFORE the hand-written table, which filed CLAUDE_CODE_ATTRIBUTION_HEADER — a variable swisscode sets — as an "unreleased codename" because it is two words. A verified description now outranks a guess about a name's shape. Extraction is a committed script (`npm run extract:claude-env`), not a pasted snapshot: the catalog ships in the package, so the build must not require Claude Code installed, and without the script it would be a list nobody could regenerate against a binary that ships weekly. The UI prints the version it came from. Served from its own route rather than `bootstrap`: 57 kB that one screen wants, and a cold start should not pay for a screen nobody opened. 778 tests, 131.9 kB packed (ceiling 150). Signed-off-by: jellologic <31935831+jellologic@users.noreply.github.com> --- package.json | 5 +- scripts/extract-claude-env.mjs | 256 ++ .../agents/claude-code/env-catalog.ts | 2254 +++++++++++++++++ src/adapters/web/api.ts | 9 + test/adapters/claude-env-catalog.test.ts | 101 + web/src/App.tsx | 5 +- web/src/api.ts | 25 + web/src/routes/Environment.tsx | 219 ++ 8 files changed, 2871 insertions(+), 3 deletions(-) create mode 100644 scripts/extract-claude-env.mjs create mode 100644 src/adapters/agents/claude-code/env-catalog.ts create mode 100644 test/adapters/claude-env-catalog.test.ts create mode 100644 web/src/routes/Environment.tsx diff --git a/package.json b/package.json index b2f3953..d5e6704 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "swisscode", "version": "0.4.0", - "description": "Drop-in launcher for Claude Code, Kilo & OpenCode. Run multiple Claude Pro/Max accounts, check usage limits and switch accounts without /login \u2014 or point any coding CLI at Ollama (local, no key), OpenRouter, z.ai/GLM, DeepSeek & Qwen. No proxy, no daemon.", + "description": "Drop-in launcher for Claude Code, Kilo & OpenCode. Run multiple Claude Pro/Max accounts, check usage limits and switch accounts without /login — or point any coding CLI at Ollama (local, no key), OpenRouter, z.ai/GLM, DeepSeek & Qwen. No proxy, no daemon.", "type": "module", "bin": { "swisscode": "./bin/swisscode.js" @@ -26,7 +26,8 @@ "prepublishOnly": "node build.js", "size": "node scripts/size-budget.js", "dev:web": "vite --config web/vite.config.ts", - "typecheck:web": "tsc --noEmit -p web/tsconfig.json" + "typecheck:web": "tsc --noEmit -p web/tsconfig.json", + "extract:claude-env": "node scripts/extract-claude-env.mjs > src/adapters/agents/claude-code/env-catalog.ts" }, "keywords": [ "claude", diff --git a/scripts/extract-claude-env.mjs b/scripts/extract-claude-env.mjs new file mode 100644 index 0000000..86db78e --- /dev/null +++ b/scripts/extract-claude-env.mjs @@ -0,0 +1,256 @@ +// Extract every environment variable a Claude Code build references. +// +// WHY A SCRIPT AND NOT A PASTED SNAPSHOT. The catalog it produces is committed +// (the build must not require Claude Code to be installed), so without this it +// would be a list nobody could regenerate, aging silently against a binary that +// ships weekly. Re-run it against a newer Claude Code and diff. +// +// node scripts/extract-claude-env.mjs > src/adapters/agents/claude-code/env-catalog.ts +// +// WHAT THIS CANNOT DO, stated up front because the output is a UI surface: +// it reads STRINGS OUT OF A BINARY. That yields names, and nothing else. It +// cannot yield meaning, defaults, accepted values, or whether a variable is +// still wired to anything. Every description in the output comes from the +// hand-maintained table below — never from the binary — and anything absent +// from that table ships explicitly marked as undocumented rather than given a +// plausible-sounding guess. A wrong description is worse than no description: +// it is a claim someone will act on. + +import { execFileSync } from 'node:child_process' +import { existsSync, readdirSync, statSync } from 'node:fs' +import { homedir } from 'node:os' +import { join } from 'node:path' + +/** Where the native installer puts versioned binaries. */ +function findBinary() { + const explicit = process.argv[2] + if (explicit) return explicit + const dir = join(homedir(), '.local', 'share', 'claude', 'versions') + if (!existsSync(dir)) return null + const versions = readdirSync(dir) + .filter((v) => /^\d+\.\d+\.\d+$/.test(v)) + .sort((a, b) => { + const pa = a.split('.').map(Number) + const pb = b.split('.').map(Number) + for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pb[i] - pa[i] + return 0 + }) + for (const v of versions) { + const candidate = join(dir, v) + if (existsSync(candidate) && statSync(candidate).isFile()) return candidate + } + return null +} + +const binary = findBinary() +if (!binary) { + console.error( + 'extract-claude-env: no Claude Code binary found. Pass one explicitly:\n' + + ' node scripts/extract-claude-env.mjs /path/to/claude', + ) + process.exit(2) +} + +const version = binary.split('/').pop() + +const raw = execFileSync('strings', ['-a', binary], { + encoding: 'utf8', + maxBuffer: 1024 * 1024 * 1024, +}) + +// Anthropic's two prefixes. `CLAUDE_` alone is deliberately excluded: it also +// matches CLAUDE_CONFIG_DIR-style host variables and a long tail of one-off +// internal names, and widening the net does not make the result more useful. +const names = [...new Set(raw.match(/\b(?:CLAUDE_CODE|ANTHROPIC)_[A-Z0-9_]+\b/g) ?? [])].sort() + +/** + * Names that clearly are not a user-facing knob, by SHAPE rather than by + * judgement about any individual one. + * + * Each rule is something you can check yourself against the list, which matters + * because this classification is the only thing standing between a browsable + * catalog and 428 undifferentiated strings. + */ +const INTERNAL = [ + { re: /_FOR_TESTING$|^CLAUDE_CODE_TEST_|_FIXTURE$/, why: 'test hook' }, + { re: /^CLAUDE_CODE_(MOCK|SIMULATE|FORCE_TIP_ID|OVERRIDE_DATE)/, why: 'test hook' }, + { re: /^CLAUDE_CODE_(BENCH|PERFETTO|FRAME_TIMING|PROFILE_(QUERY|STARTUP)|DEBUG_REPAINTS)/, why: 'profiling' }, + { re: /^ANTHROPIC_(BEDROCK|VERTEX|FOUNDRY|AWS|GOOGLE_CLOUD)/, why: 'third-party cloud provider' }, + { re: /^CLAUDE_CODE_(USE_BEDROCK|USE_VERTEX|USE_FOUNDRY|USE_MANTLE|USE_ANTHROPIC_AWS|USE_ANTHROPIC_GOOGLE_CLOUD|SKIP_.*_AUTH|SKIP_AWS_CRED_CACHE)/, why: 'third-party cloud provider' }, +] + +/** + * Two unrelated words joined by an underscore, with nothing else in the name. + * + * Anthropic ships unreleased features behind codenames — ALDER_WICKET, + * BISON_CAIRN, PEWTER_OWL, THISTLE_GREBE. They are indistinguishable from real + * knobs by name alone, they appear and vanish between releases, and a user who + * sets one is configuring something nobody can describe. Matched structurally + * so the rule keeps working on names that do not exist yet. + */ +const CODENAME = /^CLAUDE_CODE_[A-Z]+_[A-Z]+$/ + +// Words that make a two-word name a real knob rather than a codename. Without +// this, TMUX_PREFIX and MAX_RETRIES would be classified as codenames. +const REAL_WORDS = + /(MAX|MIN|DISABLE|ENABLE|SKIP|FORCE|USE|API|OAUTH|TOKEN|MODEL|PROXY|TIMEOUT|SESSION|DEBUG|LOG|DIR|PATH|FILE|URL|KEY|MODE|TMUX|SHELL|PLUGIN|SYNC|IDE|OTEL|MCP|OUTPUT|CONTEXT|TOOL|AGENT|MEMORY|REMOTE|SANDBOX|TELEMETRY|VERSION|PROMPT|RETRY|RETRIES|CACHE|GLOB|EFFORT|SUBAGENT|TERMINAL|ARTIFACT|WORKFLOW|ENTRYPOINT|EMAIL|UUID|ID)/ + +/** + * The hand-written half. NOTHING HERE COMES FROM THE BINARY. + * + * Every entry is a variable whose behaviour is documented by Anthropic, visible + * in `claude --help`, or already relied on by swisscode's own adapter — which + * is to say, one somebody can be held to. Everything not in this table ships as + * `undocumented`, name only. + */ +const DESCRIBED = { + ANTHROPIC_API_KEY: ['credential', 'The API key Claude Code authenticates with. swisscode sets this per profile; it is cleared for any launch not going to first-party Anthropic.'], + ANTHROPIC_AUTH_TOKEN: ['credential', 'Bearer-token form of the credential, used by most gateways. swisscode picks this or ANTHROPIC_API_KEY based on the provider descriptor.'], + ANTHROPIC_BASE_URL: ['endpoint', 'The Anthropic-compatible endpoint to talk to. swisscode sets this from the profile’s provider; a bare host, with no /v1.'], + ANTHROPIC_MODEL: ['model', 'Overrides the model for the session. Prefer the per-tier ANTHROPIC_DEFAULT_*_MODEL variables, which is what swisscode pins.'], + ANTHROPIC_SMALL_FAST_MODEL: ['model', 'The model used for cheap background work. Superseded by ANTHROPIC_DEFAULT_HAIKU_MODEL on current builds.'], + ANTHROPIC_DEFAULT_OPUS_MODEL: ['model', 'Model id for the opus tier. Accepts a [1m] suffix to request the extended context window.'], + ANTHROPIC_DEFAULT_SONNET_MODEL: ['model', 'Model id for the sonnet tier.'], + ANTHROPIC_DEFAULT_HAIKU_MODEL: ['model', 'Model id for the haiku tier, used for background and summarisation work.'], + ANTHROPIC_DEFAULT_FABLE_MODEL: ['model', 'Model id for the fable tier.'], + ANTHROPIC_CUSTOM_HEADERS: ['endpoint', 'Extra HTTP headers on every API request, as newline-separated Name: Value pairs.'], + ANTHROPIC_BETAS: ['endpoint', 'Beta headers to include in API requests. API-key users only.'], + ANTHROPIC_LOG: ['debug', 'Log verbosity for the API client.'], + CLAUDE_CONFIG_DIR: ['session', 'Which directory holds the login and settings. THE BRANCH IS ON WHETHER THIS IS SET, not on its value — setting it to the default ~/.claude is a different, empty login.'], + CLAUDE_CODE_MAX_OUTPUT_TOKENS: ['limits', 'Ceiling on tokens the model may produce in one response.'], + CLAUDE_CODE_MAX_CONTEXT_TOKENS: ['limits', 'Ceiling on the context window Claude Code will fill before compacting.'], + CLAUDE_CODE_MAX_RETRIES: ['limits', 'How many times a failed API request is retried.'], + CLAUDE_CODE_MAX_TURNS: ['limits', 'Maximum agent turns before the session stops on its own.'], + CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS: ['limits', 'How many subagents may run at once.'], + CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY: ['limits', 'How many tool calls may run in parallel within one turn.'], + CLAUDE_CODE_SUBAGENT_MODEL: ['model', 'Model subagents run on. Pinning it matters on gateways: subagents 404 when the id is not one the endpoint serves. swisscode sets this for OpenRouter.'], + CLAUDE_CODE_AUTO_COMPACT_WINDOW: ['limits', 'Fraction of the context window at which auto-compaction triggers.'], + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: ['traffic', 'Stops background requests an endpoint may not serve. ALSO disables gateway model discovery, so every tier must be pinned explicitly.'], + CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING: ['compat', 'Works around gateways that reject the adaptive thinking input tag with 400.'], + CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: ['compat', 'Works around gateways that reject unknown beta fields with "Extra inputs are not permitted".'], + CLAUDE_CODE_SKIP_FAST_MODE_ORG_CHECK: ['compat', 'Skips the organisation check that reports fast mode as disabled on non-first-party endpoints.'], + CLAUDE_CODE_ATTRIBUTION_HEADER: ['traffic', 'Set to 0 to drop the attribution header, which improves prompt-cache hit rate through some gateways.'], + CLAUDE_CODE_DISABLE_1M_CONTEXT: ['limits', 'Turns off the 1M context window request.'], + CLAUDE_CODE_DISABLE_THINKING: ['behaviour', 'Disables extended thinking.'], + CLAUDE_CODE_DISABLE_TERMINAL_TITLE: ['ui', 'Stops Claude Code rewriting the terminal window title.'], + CLAUDE_CODE_DISABLE_MOUSE: ['ui', 'Disables mouse reporting, which some terminals and multiplexers handle badly.'], + CLAUDE_CODE_DISABLE_AUTO_MEMORY: ['behaviour', 'Stops automatic memory capture.'], + CLAUDE_CODE_DISABLE_CLAUDE_MDS: ['behaviour', 'Stops CLAUDE.md files being discovered and loaded.'], + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: ['behaviour', 'Disables background task execution.'], + CLAUDE_CODE_ENABLE_TELEMETRY: ['telemetry', 'Enables OpenTelemetry export.'], + CLAUDE_CODE_SAFE_MODE: ['behaviour', 'Set by --safe-mode. Disables customisations — CLAUDE.md, skills, plugins, hooks, MCP servers — for troubleshooting a broken configuration.'], + CLAUDE_CODE_SIMPLE: ['behaviour', 'Set by --bare. Skips hooks, LSP, plugin sync, auto-memory, keychain reads and CLAUDE.md discovery.'], + CLAUDE_CODE_EFFORT_LEVEL: ['behaviour', 'Effort level for the session: low, medium, high, xhigh, max. Also settable with --effort.'], + CLAUDE_CODE_SHELL: ['environment', 'Which shell the Bash tool uses.'], + CLAUDE_CODE_TMPDIR: ['environment', 'Temporary directory Claude Code writes to.'], + CLAUDE_CODE_GIT_BASH_PATH: ['environment', 'Path to Git Bash on Windows.'], + CLAUDE_CODE_HTTP_PROXY: ['network', 'HTTP proxy for Claude Code’s own requests.'], + CLAUDE_CODE_HTTPS_PROXY: ['network', 'HTTPS proxy for Claude Code’s own requests.'], + CLAUDE_CODE_CLIENT_CERT: ['network', 'Client certificate for mTLS.'], + CLAUDE_CODE_CLIENT_KEY: ['network', 'Client key for mTLS.'], + CLAUDE_CODE_ENTRYPOINT: ['environment', 'How Claude Code was started. swisscode does not set this; the agent uses it for its own telemetry.'], + CLAUDE_CODE_VERSION: ['environment', 'The running Claude Code version.'], + CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: ['traffic', 'Lets Claude Code ask a gateway which models it serves. Disabled as a side effect of CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC.'], + CLAUDE_CODE_API_KEY_HELPER_TTL_MS: ['credential', 'How long a credential from apiKeyHelper is cached.'], + CLAUDE_CODE_OAUTH_TOKEN: ['credential', 'OAuth access token. Written by /login; swisscode reads it only to measure subscription usage, and never refreshes it.'], + CLAUDE_CODE_SUBSCRIPTION_TYPE: ['credential', 'The plan behind the current login, e.g. max.'], + CLAUDE_CODE_DEBUG_LOG_LEVEL: ['debug', 'Verbosity of debug logging. Also settable with --debug.'], + CLAUDE_CODE_DEBUG_LOGS_DIR: ['debug', 'Where debug logs are written.'], + CLAUDE_CODE_SESSION_ID: ['session', 'The current session id. Also settable with --session-id.'], + CLAUDE_CODE_MANAGED_SETTINGS_PATH: ['environment', 'Path to admin-managed policy settings.'], + ENABLE_TOOL_SEARCH: ['compat', 'Enables MCP tool search, which is off by default away from first-party Anthropic.'], + API_FORCE_IDLE_TIMEOUT: ['compat', 'Set to 0 to stop the client giving up on slow or locally hosted models.'], + MAX_THINKING_TOKENS: ['limits', 'Ceiling on tokens spent on extended thinking.'], + DISABLE_TELEMETRY: ['telemetry', 'Disables telemetry reporting.'], + DISABLE_ERROR_REPORTING: ['telemetry', 'Disables error reporting.'], + DISABLE_AUTOUPDATER: ['behaviour', 'Stops Claude Code updating itself.'], +} + +// Variables swisscode itself writes, so the UI can say "this one is already +// yours to set from a profile" instead of listing it as inert trivia. +const SWISSCODE_SETS = new Set([ + 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CONFIG_DIR', + 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', + 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_FABLE_MODEL', + 'CLAUDE_CODE_SUBAGENT_MODEL', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', + 'CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING', 'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS', + 'CLAUDE_CODE_SKIP_FAST_MODE_ORG_CHECK', 'CLAUDE_CODE_ATTRIBUTION_HEADER', + 'ENABLE_TOOL_SEARCH', 'API_FORCE_IDLE_TIMEOUT', +]) + +function classify(name) { + // THE HAND-WRITTEN TABLE OUTRANKS EVERY HEURISTIC, and the order here is the + // whole point rather than a detail. A description in DESCRIBED means somebody + // verified the variable; a regex is a guess about a name's shape. Running the + // guesses first got `CLAUDE_CODE_ATTRIBUTION_HEADER` — a variable swisscode + // sets itself — filed as an "unreleased feature codename", because it happens + // to be two words. Caught by the catalog's own tests, not by reading. + if (DESCRIBED[name]) return { kind: 'documented' } + for (const rule of INTERNAL) if (rule.re.test(name)) return { kind: 'internal', why: rule.why } + if (CODENAME.test(name) && !REAL_WORDS.test(name)) { + return { kind: 'internal', why: 'unreleased feature codename' } + } + return { kind: 'undocumented' } +} + +// Names swisscode knows about that the binary does not spell with a prefix we +// scan for (ENABLE_TOOL_SEARCH, MAX_THINKING_TOKENS, …). Included so the +// catalog is not narrower than the adapter it describes. +const extra = Object.keys(DESCRIBED).filter((n) => !names.includes(n)) +const all = [...names, ...extra].sort() + +const entries = all.map((name) => { + const c = classify(name) + const described = DESCRIBED[name] + return { + name, + kind: c.kind, + ...(c.why ? { why: c.why } : {}), + ...(described ? { category: described[0], description: described[1] } : {}), + ...(SWISSCODE_SETS.has(name) ? { managed: true } : {}), + } +}) + +const counts = entries.reduce((acc, e) => ({ ...acc, [e.kind]: (acc[e.kind] ?? 0) + 1 }), {}) + +process.stdout.write(`// GENERATED by scripts/extract-claude-env.mjs — do not edit by hand. +// +// Every environment variable referenced by Claude Code ${version}, extracted +// from the shipped binary, plus the ones swisscode's own adapter sets. +// +// READ THE 'kind' FIELD BEFORE TRUSTING AN ENTRY. Extraction yields NAMES AND +// NOTHING ELSE — no meaning, no defaults, no accepted values, and no evidence +// that a name is still wired to anything. So: +// +// documented ${String(counts.documented ?? 0).padStart(3)} described by hand from Anthropic's docs, \`claude --help\`, +// or swisscode's own adapter. Safe to act on. +// undocumented ${String(counts.undocumented ?? 0).padStart(3)} the name is real; the meaning is NOT KNOWN. Shipped +// without a description on purpose — a plausible guess is +// worse than a blank, because someone will act on it. +// internal ${String(counts.internal ?? 0).padStart(3)} test hooks, profiling switches, third-party cloud +// auth, and unreleased feature codenames. Present for +// completeness; not knobs. +// +// Regenerate against a newer Claude Code and diff: +// node scripts/extract-claude-env.mjs > src/adapters/agents/claude-code/env-catalog.ts + +/** How much is actually known about an entry. */ +export type ClaudeEnvKind = 'documented' | 'undocumented' | 'internal' + +export type ClaudeEnvVar = { + name: string + kind: ClaudeEnvKind + /** why it was classified internal; absent otherwise */ + why?: string + category?: string + /** hand-written, never extracted; absent for undocumented and internal */ + description?: string + /** swisscode's own adapter sets this one from a profile */ + managed?: boolean +} + +/** The Claude Code build this was extracted from. */ +export const CATALOG_SOURCE = ${JSON.stringify({ agent: 'claude-code', version })} as const + +export const CLAUDE_ENV_CATALOG: readonly ClaudeEnvVar[] = Object.freeze(${JSON.stringify(entries, null, 2)}) +`) diff --git a/src/adapters/agents/claude-code/env-catalog.ts b/src/adapters/agents/claude-code/env-catalog.ts new file mode 100644 index 0000000..432cddc --- /dev/null +++ b/src/adapters/agents/claude-code/env-catalog.ts @@ -0,0 +1,2254 @@ +// GENERATED by scripts/extract-claude-env.mjs — do not edit by hand. +// +// Every environment variable referenced by Claude Code 2.1.217, extracted +// from the shipped binary, plus the ones swisscode's own adapter sets. +// +// READ THE 'kind' FIELD BEFORE TRUSTING AN ENTRY. Extraction yields NAMES AND +// NOTHING ELSE — no meaning, no defaults, no accepted values, and no evidence +// that a name is still wired to anything. So: +// +// documented 60 described by hand from Anthropic's docs, `claude --help`, +// or swisscode's own adapter. Safe to act on. +// undocumented 338 the name is real; the meaning is NOT KNOWN. Shipped +// without a description on purpose — a plausible guess is +// worse than a blank, because someone will act on it. +// internal 97 test hooks, profiling switches, third-party cloud +// auth, and unreleased feature codenames. Present for +// completeness; not knobs. +// +// Regenerate against a newer Claude Code and diff: +// node scripts/extract-claude-env.mjs > src/adapters/agents/claude-code/env-catalog.ts + +/** How much is actually known about an entry. */ +export type ClaudeEnvKind = 'documented' | 'undocumented' | 'internal' + +export type ClaudeEnvVar = { + name: string + kind: ClaudeEnvKind + /** why it was classified internal; absent otherwise */ + why?: string + category?: string + /** hand-written, never extracted; absent for undocumented and internal */ + description?: string + /** swisscode's own adapter sets this one from a profile */ + managed?: boolean +} + +/** The Claude Code build this was extracted from. */ +export const CATALOG_SOURCE = {"agent":"claude-code","version":"2.1.217"} as const + +export const CLAUDE_ENV_CATALOG: readonly ClaudeEnvVar[] = Object.freeze([ + { + "name": "ANTHROPIC_API_KEY", + "kind": "documented", + "category": "credential", + "description": "The API key Claude Code authenticates with. swisscode sets this per profile; it is cleared for any launch not going to first-party Anthropic.", + "managed": true + }, + { + "name": "ANTHROPIC_AUTH_TOKEN", + "kind": "documented", + "category": "credential", + "description": "Bearer-token form of the credential, used by most gateways. swisscode picks this or ANTHROPIC_API_KEY based on the provider descriptor.", + "managed": true + }, + { + "name": "ANTHROPIC_AWS_API_KEY", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_AWS_BASE_URL", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_AWS_WORKSPACE_ID", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_BASE_URL", + "kind": "documented", + "category": "endpoint", + "description": "The Anthropic-compatible endpoint to talk to. swisscode sets this from the profile’s provider; a bare host, with no /v1.", + "managed": true + }, + { + "name": "ANTHROPIC_BEDROCK_BASE_URL", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_BEDROCK_MANTLE_BASE_URL", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_BEDROCK_SERVICE_TIER", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_BETAS", + "kind": "documented", + "category": "endpoint", + "description": "Beta headers to include in API requests. API-key users only." + }, + { + "name": "ANTHROPIC_CONFIG_DIR", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_CUSTOM_HEADERS", + "kind": "documented", + "category": "endpoint", + "description": "Extra HTTP headers on every API request, as newline-separated Name: Value pairs." + }, + { + "name": "ANTHROPIC_CUSTOM_MODEL_OPTION", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_CUSTOM_MODEL_OPTION_NAME", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_FABLE_MODEL", + "kind": "documented", + "category": "model", + "description": "Model id for the fable tier.", + "managed": true + }, + { + "name": "ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "kind": "documented", + "category": "model", + "description": "Model id for the haiku tier, used for background and summarisation work.", + "managed": true + }, + { + "name": "ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_OPUS_MODEL", + "kind": "documented", + "category": "model", + "description": "Model id for the opus tier. Accepts a [1m] suffix to request the extended context window.", + "managed": true + }, + { + "name": "ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_SONNET_MODEL", + "kind": "documented", + "category": "model", + "description": "Model id for the sonnet tier.", + "managed": true + }, + { + "name": "ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_ENVIRONMENT_ID", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_ENVIRONMENT_KEY", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_FEDERATION_RULE_ID", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_FOUNDRY_API_KEY", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_FOUNDRY_AUTH_TOKEN", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_FOUNDRY_BASE_URL", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_FOUNDRY_RESOURCE", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_GOOGLE_CLOUD_BASE_URL", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_GOOGLE_CLOUD_LOCATION", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_GOOGLE_CLOUD_PROJECT", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_GOOGLE_CLOUD_WORKSPACE_ID", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_IDENTITY_TOKEN", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_IDENTITY_TOKEN_FILE", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_LOG", + "kind": "documented", + "category": "debug", + "description": "Log verbosity for the API client." + }, + { + "name": "ANTHROPIC_MODEL", + "kind": "documented", + "category": "model", + "description": "Overrides the model for the session. Prefer the per-tier ANTHROPIC_DEFAULT_*_MODEL variables, which is what swisscode pins." + }, + { + "name": "ANTHROPIC_ORGANIZATION_ID", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_PROFILE", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_SCOPE", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_SERVICE_ACCOUNT_ID", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_SESSION_ID", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_SMALL_FAST_MODEL", + "kind": "documented", + "category": "model", + "description": "The model used for cheap background work. Superseded by ANTHROPIC_DEFAULT_HAIKU_MODEL on current builds." + }, + { + "name": "ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_UNIX_SOCKET", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_VERTEX_BASE_URL", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_VERTEX_PROJECT_ID", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "ANTHROPIC_WEBHOOK_SIGNING_KEY", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_WORKSPACE_ID", + "kind": "undocumented" + }, + { + "name": "ANTHROPIC_WORK_ID", + "kind": "undocumented" + }, + { + "name": "API_FORCE_IDLE_TIMEOUT", + "kind": "documented", + "category": "compat", + "description": "Set to 0 to stop the client giving up on slow or locally hosted models.", + "managed": true + }, + { + "name": "CLAUDE_CODE_3P_PROBE_WROTE_OPUS_DEFAULT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_3P_PROBE_WROTE_SONNET_DEFAULT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ACCESSIBILITY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ACCOUNT_TAGGED_ID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ACCOUNT_UUID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ACTION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ACT_DONT_REDERIVE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ADDITIONAL_PROTECTION", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_AGENT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AGENT_PROXY_GH_SHIM", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AGENT_PROXY_GIT_CONFIG", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AGENT_RULE_DISABLED", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AGENT_VIEW_RELAUNCH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ALDER_WICKET", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_ALTGR_AS_TEXT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ALT_SCREEN_FULL_REPAINT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AMBER_ASTROLABE", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_API_BASE_URL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_API_KEY_HELPER_TTL_MS", + "kind": "documented", + "category": "credential", + "description": "How long a credential from apiKeyHelper is cached." + }, + { + "name": "CLAUDE_CODE_ARTIFACT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ARTIFACTS_API_BASE_URL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ARTIFACT_AUTO_OPEN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ARTIFACT_DIRECT_UPLOAD", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ARTIFACT_MCP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ATTRIBUTION_HEADER", + "kind": "documented", + "category": "traffic", + "description": "Set to 0 to drop the attribution header, which improves prompt-cache hit rate through some gateways.", + "managed": true + }, + { + "name": "CLAUDE_CODE_AUTH_FAIL_EXIT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_COMPACT_WINDOW", + "kind": "documented", + "category": "limits", + "description": "Fraction of the context window at which auto-compaction triggers." + }, + { + "name": "CLAUDE_CODE_AUTO_CONNECT_IDE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_CLASSIFY_EDITS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_EDIT_REMOVAL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_EDIT_REMOVAL_CAP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_EXTERNAL_PERMISSIONS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_GIT_STATUS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_GIT_STATUS_LIMIT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_GIT_STATUS_UPLOADS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_MODEL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_OUTCOME_CODES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_PRIOR_ASSISTANT_CONTEXT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_REPO_VISIBILITY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_SIBLING_CONTEXT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AUTO_MODE_TEMPERATURE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_AWS_CHAIN_RESOLVE_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BASALT_COVE", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_BASE_REF", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_BASE_REFS", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_BASH_SANDBOX_SHOW_INDICATOR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BENCH_LIVE_COUNTS", + "kind": "internal", + "why": "profiling" + }, + { + "name": "CLAUDE_CODE_BG_CLASSIFIER_MODEL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BG_TASKS_REPORT_RUNNING", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BISON_CAIRN", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BRIDGE_SESSION_ID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BRIEF", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BRIEF_UPLOAD", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_BS_AS_CTRL_BACKSPACE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BUBBLEWRAP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_BYOC_ENABLE_DATADOG", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_CERT_STORE", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_CHILD_SESSION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_CLASSIFIER_SUMMARY", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_CLIENT_CERT", + "kind": "documented", + "category": "network", + "description": "Client certificate for mTLS." + }, + { + "name": "CLAUDE_CODE_CLIENT_KEY", + "kind": "documented", + "category": "network", + "description": "Client key for mTLS." + }, + { + "name": "CLAUDE_CODE_CLIENT_KEY_PASSPHRASE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_COLD_COMPACT", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_COMMIT_LOG", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_CONTAINER_ID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_COORDINATOR_EXTRA_TOOLS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_COORDINATOR_MODE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_COORDINATOR_PROPAGATE_NESTED_MEMORY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_CUSTOM_OAUTH_URL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DAEMON_COLD_START", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DATADOG_FLUSH_INTERVAL_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DD_ERROR_TRACKING_FLUSH_INTERVAL_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DEBUG_LOGS_DIR", + "kind": "documented", + "category": "debug", + "description": "Where debug logs are written." + }, + { + "name": "CLAUDE_CODE_DEBUG_LOG_LEVEL", + "kind": "documented", + "category": "debug", + "description": "Verbosity of debug logging. Also settable with --debug." + }, + { + "name": "CLAUDE_CODE_DEBUG_REPAINTS", + "kind": "internal", + "why": "profiling" + }, + { + "name": "CLAUDE_CODE_DECSTBM", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DESIGN_OAUTH_CLIENT_ID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DEV_RAW_CHANGELOG_URL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DIAGNOSTICS_FILE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_1M_CONTEXT", + "kind": "documented", + "category": "limits", + "description": "Turns off the 1M context window request." + }, + { + "name": "CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING", + "kind": "documented", + "category": "compat", + "description": "Works around gateways that reject the adaptive thinking input tag with 400.", + "managed": true + }, + { + "name": "CLAUDE_CODE_DISABLE_ADVISOR_TOOL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_AGENT_VIEW", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_ARTIFACT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_ATTACHMENTS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_AUTO_MEMORY", + "kind": "documented", + "category": "behaviour", + "description": "Stops automatic memory capture." + }, + { + "name": "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS", + "kind": "documented", + "category": "behaviour", + "description": "Disables background task execution." + }, + { + "name": "CLAUDE_CODE_DISABLE_BEDROCK_CONTENT_TYPE_GUARD", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_BUNDLED_SKILLS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_CLAUDE_CODE_SKILL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_CLAUDE_MDS", + "kind": "documented", + "category": "behaviour", + "description": "Stops CLAUDE.md files being discovered and loaded." + }, + { + "name": "CLAUDE_CODE_DISABLE_CRON", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", + "kind": "documented", + "category": "compat", + "description": "Works around gateways that reject unknown beta fields with \"Extra inputs are not permitted\".", + "managed": true + }, + { + "name": "CLAUDE_CODE_DISABLE_EXPLORE_INHERIT_CAP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_FAST_MODE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_LAUNCH_COMPOSER", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_MEMORY_BULK_INFLATE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_MEMORY_PERIODIC_RESYNC", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_MEMORY_STREAM_LIST", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_MOUSE", + "kind": "documented", + "category": "ui", + "description": "Disables mouse reporting, which some terminals and multiplexers handle badly." + }, + { + "name": "CLAUDE_CODE_DISABLE_MOUSE_CLICKS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_NESTED_CHAIN_IDLE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", + "kind": "documented", + "category": "traffic", + "description": "Stops background requests an endpoint may not serve. ALSO disables gateway model discovery, so every tier must be pinned explicitly.", + "managed": true + }, + { + "name": "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_NOTIFICATION_PRESENCE_CHECK", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_ORG_MEMORY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_POLICY_SKILLS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_PRECOMPACT_SKIP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_REFUSAL_FALLBACK", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_TERMINAL_TITLE", + "kind": "documented", + "category": "ui", + "description": "Stops Claude Code rewriting the terminal window title." + }, + { + "name": "CLAUDE_CODE_DISABLE_THINKING", + "kind": "documented", + "category": "behaviour", + "description": "Disables extended thinking." + }, + { + "name": "CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_WORKFLOWS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DISABLE_WORKING_SYNC", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DONT_INHERIT_ENV", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_DOWNLOAD_DEADLINE_MS_FOR_TESTING", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_EAGER_FLUSH", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_EFFORT_LEVEL", + "kind": "documented", + "category": "behaviour", + "description": "Effort level for the session: low, medium, high, xhigh, max. Also settable with --effort." + }, + { + "name": "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_APPEND_SUBAGENT_PROMPT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_AUTO_MODE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_AWAY_SUMMARY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_BACKGROUND_PLUGIN_REFRESH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_CFC", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_DESIGN_SYNC", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_EXPERIMENTAL_ADVISOR_TOOL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_FEEDBACK_SURVEY_FOR_OTEL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", + "kind": "documented", + "category": "traffic", + "description": "Lets Claude Code ask a gateway which models it serves. Disabled as a side effect of CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC." + }, + { + "name": "CLAUDE_CODE_ENABLE_LAUNCH_COMPOSER", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_MENU_KIND_LANES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_PROXY_AUTH_HELPER", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_REFRESH_MCP_TOOLS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_REMOTE_RECAP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_TASKS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_TELEMETRY", + "kind": "documented", + "category": "telemetry", + "description": "Enables OpenTelemetry export." + }, + { + "name": "CLAUDE_CODE_ENABLE_TOKEN_USAGE_ATTACHMENT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENABLE_XAA", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ENTRYPOINT", + "kind": "documented", + "category": "environment", + "description": "How Claude Code was started. swisscode does not set this; the agent uses it for its own telemetry." + }, + { + "name": "CLAUDE_CODE_ENVIRONMENT_KIND", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_ENVIRONMENT_RUNNER_VERSION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_EXECPATH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_EXIT_AFTER_STOP_DELAY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_EXPERIMENTAL_OBSERVER_AGENTS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_EXTRA_BODY", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_EXTRA_METADATA", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_FABLE_BRIDGE_DIALOG_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FLEETVIEW_SIMPLE", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_FLEET_PAST_SESSIONS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_BRIDGE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_EVALUATE_MEMORY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_FULLSCREEN_UPSELL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_FULL_LOGO", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_MEMORY_SURVEY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_MID_CONVERSATION_SYSTEM", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_RC_LONG_TURN_NUDGE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_SANDBOX", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_SESSION_PERSISTENCE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_STRIKETHROUGH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_SYNC_OUTPUT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORCE_TIP_ID", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_FORCE_WINDOWS_CREDMAN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORK_SUBAGENT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FORWARD_SUBAGENT_TEXT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_FRAME_TIMING_LOG", + "kind": "internal", + "why": "profiling" + }, + { + "name": "CLAUDE_CODE_FRAME_TIMING_SAMPLE_EVERY", + "kind": "internal", + "why": "profiling" + }, + { + "name": "CLAUDE_CODE_GAULT_KESTREL", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_GB_BASE_URL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_GB_REFRESH_INTERVAL_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_GIT_BASH_PATH", + "kind": "documented", + "category": "environment", + "description": "Path to Git Bash on Windows." + }, + { + "name": "CLAUDE_CODE_GLOB_HIDDEN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_GLOB_NO_IGNORE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_GLOB_TIMEOUT_SECONDS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_GZIP_REQUEST_BODIES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HERON_TALLOW", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_HFI_BEARER_TOKEN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HIDE_CWD", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HIDE_SETTINGS_HINT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HOST_AUTH_ENV_VAR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HOST_AUTH_REFRESH_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HOST_CREDS_FILE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HOST_HTTP_PROXY_PORT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HOST_PLATFORM", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_HOST_SOCKS_PROXY_PORT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_HTTPS_PROXY", + "kind": "documented", + "category": "network", + "description": "HTTPS proxy for Claude Code’s own requests." + }, + { + "name": "CLAUDE_CODE_HTTP_PROXY", + "kind": "documented", + "category": "network", + "description": "HTTP proxy for Claude Code’s own requests." + }, + { + "name": "CLAUDE_CODE_IDE_HOST_OVERRIDE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_IDE_SKIP_VALID_CHECK", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_IDLE_THRESHOLD_MINUTES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_IDLE_TOKEN_THRESHOLD", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_INCLUDE_PARTIAL_MESSAGES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_INVESTIGATE_FIRST", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_INVOKED_SKILLS", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_IS_COWORK", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_JSONL_TRANSCRIPT", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_KB_COHESION_FIXES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_LANTERN_PRISM", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_LARCH_CISTERN", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_LOOP_KEEPALIVE", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_LOOP_PERSISTENT", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_MANAGED_SETTINGS_PATH", + "kind": "documented", + "category": "environment", + "description": "Path to admin-managed policy settings." + }, + { + "name": "CLAUDE_CODE_MARL_CORMORANT", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS", + "kind": "documented", + "category": "limits", + "description": "How many subagents may run at once." + }, + { + "name": "CLAUDE_CODE_MAX_CONTEXT_TOKENS", + "kind": "documented", + "category": "limits", + "description": "Ceiling on the context window Claude Code will fill before compacting." + }, + { + "name": "CLAUDE_CODE_MAX_OUTPUT_TOKENS", + "kind": "documented", + "category": "limits", + "description": "Ceiling on tokens the model may produce in one response." + }, + { + "name": "CLAUDE_CODE_MAX_RETRIES", + "kind": "documented", + "category": "limits", + "description": "How many times a failed API request is retried." + }, + { + "name": "CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY", + "kind": "documented", + "category": "limits", + "description": "How many tool calls may run in parallel within one turn." + }, + { + "name": "CLAUDE_CODE_MAX_TURNS", + "kind": "documented", + "category": "limits", + "description": "Maximum agent turns before the session stops on its own." + }, + { + "name": "CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MCP_ALLOWLIST_ENV", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MCP_SERVER_NAME", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MCP_SERVER_URL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MEMORY_PUSH_DELETE_MODE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_MOCK_REMOTE_SETTINGS", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_MOCK_TRIAL", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_NANKEEN_KESTREL", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_NATIVE_CURSOR", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_NEW_INIT", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_NO_FLICKER", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_NO_MODEL_FALLBACK", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OAUTH_401_WAIT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OAUTH_CLIENT_ID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OAUTH_REFRESH_TOKEN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OAUTH_SCOPES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OAUTH_TOKEN", + "kind": "documented", + "category": "credential", + "description": "OAuth access token. Written by /login; swisscode reads it only to measure subscription usage, and never refreshes it." + }, + { + "name": "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ORGANIZATION_UUID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OTEL_DIAG_STDERR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OTEL_FLUSH_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_OVERRIDE_DATE", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PARKED_PERMISSION_WAIT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PERFETTO_TRACE", + "kind": "internal", + "why": "profiling" + }, + { + "name": "CLAUDE_CODE_PERFETTO_WRITE_INTERVAL_S", + "kind": "internal", + "why": "profiling" + }, + { + "name": "CLAUDE_CODE_PERFORCE_MODE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PEWTER_OWL", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_PEWTER_OWL_TOOL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLAN_MODE_REQUIRED", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLAN_V2_AGENT_COUNT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLAN_V2_EXPLORE_AGENT_COUNT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLUGIN_BINARY_ASSETS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLUGIN_CACHE_DIR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLUGIN_GIT_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLUGIN_PREFER_HTTPS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLUGIN_SEED_DIR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PLUGIN_USE_ZIP_CACHE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_POWERSHELL_RESPECT_EXECUTION_POLICY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_POWERUP_ONBOARDING", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PROACTIVE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PROCESS_WRAPPER", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_PROFILE_QUERY", + "kind": "internal", + "why": "profiling" + }, + { + "name": "CLAUDE_CODE_PROFILE_STARTUP", + "kind": "internal", + "why": "profiling" + }, + { + "name": "CLAUDE_CODE_PROPAGATE_TRACEPARENT", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PROXY_AUTHENTICATE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PROXY_AUTH_HELPER_TTL_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PROXY_HOST", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PROXY_RESOLVES_HOSTS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PROXY_URL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_PWSH_PARSE_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_QUESTION_PREVIEW_FORMAT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RATE_LIMIT_TIER", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RC_PERMISSION_NUDGE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REFUSAL_FALLBACK_CATCH_ALL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RELAUNCH_TERMINAL_SIZE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_ENVIRONMENT_TYPE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_HERMETIC_MODE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_MEMORY_DIR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_RAW_EVENTS_FILE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_SEND_KEEPALIVES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_SESSION_ID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_SESSION_ORIGIN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_SETTINGS_PATH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REMOTE_SETTINGS_POLL_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REPL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_REPORT_FINDINGS", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_REPO_CHECKOUTS", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_RESUME_FROM_SESSION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RESUME_INTERRUPTED_TURN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RESUME_INTERRUPTED_TURN_MAX_AGE_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RESUME_PROMPT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RESUME_SOURCE_ALIVE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RESUME_THRESHOLD_MINUTES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RESUME_TOKEN_THRESHOLD", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_RETRY_WATCHDOG", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SAFE_MODE", + "kind": "documented", + "category": "behaviour", + "description": "Set by --safe-mode. Disables customisations — CLAUDE.md, skills, plugins, hooks, MCP servers — for troubleshooting a broken configuration." + }, + { + "name": "CLAUDE_CODE_SANDBOXED", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SCRIPT_CAPS", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_SCROLL_SPEED", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_SDK_HAS_HOST_AUTH_REFRESH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SDK_HAS_OAUTH_REFRESH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SEND_FEEDBACK", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SESSION_ACCESS_TOKEN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SESSION_ID", + "kind": "documented", + "category": "session", + "description": "The current session id. Also settable with --session-id." + }, + { + "name": "CLAUDE_CODE_SESSION_KIND", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SESSION_LOG", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SESSION_NAME", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SHELL", + "kind": "documented", + "category": "environment", + "description": "Which shell the Bash tool uses." + }, + { + "name": "CLAUDE_CODE_SHELL_PREFIX", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SIMPLE", + "kind": "documented", + "category": "behaviour", + "description": "Set by --bare. Skips hooks, LSP, plugin sync, auto-memory, keychain reads and CLAUDE.md discovery." + }, + { + "name": "CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SIMULATE_PROXY_USAGE", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_SKILL_NAME", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_SKIP_AWS_CRED_CACHE", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_SKIP_BEDROCK_AUTH", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_SKIP_FAST_MODE_NETWORK_ERRORS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SKIP_FAST_MODE_ORG_CHECK", + "kind": "documented", + "category": "compat", + "description": "Skips the organisation check that reports fast mode as disabled on non-first-party endpoints.", + "managed": true + }, + { + "name": "CLAUDE_CODE_SKIP_FOUNDRY_AUTH", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_SKIP_HFI_VERSION_CHECK", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SKIP_MANTLE_AUTH", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_SKIP_PLUGIN_MCP_SERVERS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SKIP_PLUGIN_MCP_SERVERS_EXCEPT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SKIP_PROJECT_BACKFILL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SKIP_PROMPT_HISTORY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SKIP_REPO_UPLOAD", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SKIP_VERTEX_AUTH", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_SLOW_OPERATION_THRESHOLD_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SPAWN_TIMESTAMP_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SSE_PORT", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_STALL_TIMEOUT_MS_FOR_TESTING", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_STOP_HOOK_BLOCK_CAP", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SUBAGENT_CACHE_EVICT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SUBAGENT_MODEL", + "kind": "documented", + "category": "model", + "description": "Model subagents run on. Pinning it matters on gateways: subagents 404 when the id is not one the endpoint serves. swisscode sets this for OpenRouter.", + "managed": true + }, + { + "name": "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SUBSCRIPTION_TYPE", + "kind": "documented", + "category": "credential", + "description": "The plan behind the current login, e.g. max." + }, + { + "name": "CLAUDE_CODE_SUPERVISED", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SUPPRESS_SESSION_ATTRIBUTION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_PLUGINS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_PLUGINS_BUFFERED_DOWNLOAD", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_PLUGINS_DOWNLOAD_STALL_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_PLUGINS_INSTALL_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_PLUGINS_MCP_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_PLUGIN_INSTALL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_SESSION_REFS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_SKILLS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_SKILLS_INSTALL_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNC_SKILLS_WAIT_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_SYNTAX_HIGHLIGHT", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_SYSTEM_PROMPT_GB_FEATURE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TAGS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TAG_ISMETA_MESSAGES", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TASK_LIST_ID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TEAMMATE_COMMAND", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_TEAM_TEARDOWN_PARK_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TEE_SDK_STDOUT", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TERMINAL_MCP_TOOLS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TERMINAL_RECORDING", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TEST_FIXTURES_ROOT", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_TEST_FORCE_DENY", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_TEST_NO_GIT_BASH", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_TEST_NO_PWSH", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_THISTLE_GREBE", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_TMPDIR", + "kind": "documented", + "category": "environment", + "description": "Temporary directory Claude Code writes to." + }, + { + "name": "CLAUDE_CODE_TMUX_PREFIX", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TMUX_PREFIX_CONFLICTS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TMUX_SESSION", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TMUX_TRUECOLOR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TODO_REMINDER_MODE", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TOTAL_TOKENS_REMINDER", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TOTAL_TOKENS_REMINDER_AFTER_USER_TURN", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TOTAL_TOKENS_REMINDER_BUDGET", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TRANSCRIPT_LOCAL_GC", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TRIGGER_ID", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TUI_JUST_SWITCHED", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_TWO_STAGE_CLASSIFIER", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_ULTRAREVIEW_PREFLIGHT_FIXTURE", + "kind": "internal", + "why": "test hook" + }, + { + "name": "CLAUDE_CODE_USER_DIALOG_TIMEOUT_MS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_USER_EMAIL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_USE_ANTHROPIC_AWS", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_USE_BEDROCK", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_USE_COWORK_PLUGINS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_USE_FOUNDRY", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_USE_GATEWAY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_USE_MANTLE", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_USE_NATIVE_FILE_SEARCH", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_USE_POWERSHELL_TOOL", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_USE_VERTEX", + "kind": "internal", + "why": "third-party cloud provider" + }, + { + "name": "CLAUDE_CODE_VERSION", + "kind": "documented", + "category": "environment", + "description": "The running Claude Code version." + }, + { + "name": "CLAUDE_CODE_VOICE_FORWARD_INTERIMS_TYPED", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_WALNUT_SPIRE", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_WEBFETCH_USE_CCR_PROXY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_WEBSEARCH_USE_CCR_PROXY", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_WORKER_EPOCH", + "kind": "internal", + "why": "unreleased feature codename" + }, + { + "name": "CLAUDE_CODE_WORKFLOWS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_WORKFLOW_SIZE_WARNING_AGENTS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_WORKFLOW_SIZE_WARNING_TOKENS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CODE_WORKSPACE_HOST_PATHS", + "kind": "undocumented" + }, + { + "name": "CLAUDE_CONFIG_DIR", + "kind": "documented", + "category": "session", + "description": "Which directory holds the login and settings. THE BRANCH IS ON WHETHER THIS IS SET, not on its value — setting it to the default ~/.claude is a different, empty login.", + "managed": true + }, + { + "name": "DISABLE_AUTOUPDATER", + "kind": "documented", + "category": "behaviour", + "description": "Stops Claude Code updating itself." + }, + { + "name": "DISABLE_ERROR_REPORTING", + "kind": "documented", + "category": "telemetry", + "description": "Disables error reporting." + }, + { + "name": "DISABLE_TELEMETRY", + "kind": "documented", + "category": "telemetry", + "description": "Disables telemetry reporting." + }, + { + "name": "ENABLE_TOOL_SEARCH", + "kind": "documented", + "category": "compat", + "description": "Enables MCP tool search, which is off by default away from first-party Anthropic.", + "managed": true + }, + { + "name": "MAX_THINKING_TOKENS", + "kind": "documented", + "category": "limits", + "description": "Ceiling on tokens spent on extended thinking." + } +]) diff --git a/src/adapters/web/api.ts b/src/adapters/web/api.ts index 22ab685..14793e2 100644 --- a/src/adapters/web/api.ts +++ b/src/adapters/web/api.ts @@ -14,6 +14,7 @@ import { toCustomProvider, validateCustomProvider } from '../../core/provider-de import { TIERS } from '../../core/tiers.ts' import { accountsUsedBy, validateAccount } from '../../core/account.ts' import { COMPAT_ENV, CREDENTIAL_ENVS } from '../agents/claude-code/env.ts' +import { CATALOG_SOURCE, CLAUDE_ENV_CATALOG } from '../agents/claude-code/env-catalog.ts' import type { AgentProfile, ConfigStorePort, @@ -357,6 +358,14 @@ export function handleApi(req: ApiRequest, deps: ApiDeps): ApiResponse { }) } + // Every environment variable Claude Code references, for the browser to + // search. Its own route rather than a field on `bootstrap`: it is ~57 kB of + // static data that only one screen wants, and paying for it on every cold + // start would be a tax on people who never open that screen. + if (resource === 'claude-env' && req.method === 'GET') { + return json(200, { source: CATALOG_SOURCE, variables: CLAUDE_ENV_CATALOG }) + } + if (resource === 'profiles') { const name = rest[0] ? decodeURIComponent(rest[0]) : null if (!name) return fail(400, 'profile name is required') diff --git a/test/adapters/claude-env-catalog.test.ts b/test/adapters/claude-env-catalog.test.ts new file mode 100644 index 0000000..4038500 --- /dev/null +++ b/test/adapters/claude-env-catalog.test.ts @@ -0,0 +1,101 @@ +// The generated catalog of Claude Code environment variables. +// +// These tests are not about the data — that is extracted and will change with +// every agent release. They are about the ONE PROPERTY that makes shipping 495 +// names defensible: nothing carries a description swisscode cannot stand +// behind. A catalog built by reading strings out of a binary is complete on +// names and silent on meaning, and the moment a plausible guess sneaks into a +// description someone acts on it. +import test from 'node:test' +import assert from 'node:assert/strict' +import { + CATALOG_SOURCE, + CLAUDE_ENV_CATALOG, +} from '../../src/adapters/agents/claude-code/env-catalog.ts' +import { COMPAT_ENV, CREDENTIAL_ENVS } from '../../src/adapters/agents/claude-code/env.ts' + +test('the catalog says which agent build it came from', () => { + // Without provenance this is a list of strings of unknown vintage. The UI + // prints it, so a reader can tell whether it describes their agent. + assert.equal(CATALOG_SOURCE.agent, 'claude-code') + assert.match(CATALOG_SOURCE.version, /^\d+\.\d+\.\d+$/) + assert.ok(CLAUDE_ENV_CATALOG.length > 100, 'a catalog this small means extraction broke') +}) + +test('NO ENTRY CARRIES A DESCRIPTION IT DID NOT EARN', () => { + // The load-bearing test. `documented` is the only kind that may describe + // itself; everything else must be blank, because the extractor cannot know + // meaning and a guess is worse than a gap. + for (const v of CLAUDE_ENV_CATALOG) { + if (v.kind === 'documented') { + assert.ok(v.description && v.description.length > 10, `${v.name} is documented but says nothing`) + assert.ok(v.category, `${v.name} is documented but uncategorised`) + } else { + assert.equal(v.description, undefined, `${v.name} is ${v.kind} but carries a description`) + } + } +}) + +test('every internal entry says WHY it is internal', () => { + // "Internal" is a claim about someone else's software. It has to be + // falsifiable by a reader, not a shrug. + for (const v of CLAUDE_ENV_CATALOG.filter((x) => x.kind === 'internal')) { + assert.ok(v.why && v.why.length > 3, `${v.name} is internal with no reason given`) + } +}) + +test('unreleased feature codenames are not offered as knobs', () => { + // Anthropic ships unreleased work behind two-word codenames. They are + // indistinguishable from real settings by name, and they appear and vanish + // between releases — so a user must not be invited to set one. + const byName = new Map(CLAUDE_ENV_CATALOG.map((v) => [v.name, v])) + for (const name of ['CLAUDE_CODE_ALDER_WICKET', 'CLAUDE_CODE_BISON_CAIRN', 'CLAUDE_CODE_PEWTER_OWL']) { + const found = byName.get(name) + if (!found) continue // a later agent build may drop it; that is fine + assert.equal(found.kind, 'internal', `${name} should not be presented as a setting`) + assert.match(String(found.why), /codename/) + } +}) + +test('test hooks and cloud-provider auth are not presented as knobs either', () => { + for (const v of CLAUDE_ENV_CATALOG) { + if (/_FOR_TESTING$|^CLAUDE_CODE_TEST_/.test(v.name)) { + assert.equal(v.kind, 'internal', `${v.name} is a test hook`) + } + if (/^ANTHROPIC_(BEDROCK|VERTEX|FOUNDRY|AWS)/.test(v.name)) { + assert.equal(v.kind, 'internal', `${v.name} is third-party cloud auth`) + } + } +}) + +test('every variable swisscode itself sets is in the catalog, described and flagged', () => { + // The catalog would be actively misleading if it omitted the variables the + // launcher writes: someone would set one by hand in a profile's `env` block + // and quietly fight the adapter that already owns it. + const byName = new Map(CLAUDE_ENV_CATALOG.map((v) => [v.name, v])) + const ours = [...Object.values(COMPAT_ENV).map((e) => e.env), ...CREDENTIAL_ENVS, 'ANTHROPIC_BASE_URL'] + for (const name of ours) { + const found = byName.get(name) + assert.ok(found, `${name} is set by the adapter but missing from the catalog`) + assert.equal(found.kind, 'documented', `${name} is set by swisscode but undescribed`) + assert.equal(found.managed, true, `${name} should be flagged as swisscode-managed`) + } +}) + +test('names are unique and shaped like environment variables', () => { + const seen = new Set() + for (const v of CLAUDE_ENV_CATALOG) { + assert.ok(!seen.has(v.name), `${v.name} appears twice`) + seen.add(v.name) + assert.match(v.name, /^[A-Z][A-Z0-9_]*$/, `${v.name} is not an env var name`) + } +}) + +test('the catalog holds no values, only names — it describes, it does not configure', () => { + // A catalog that carried values would be a second place to configure the + // agent, competing with profiles. It is documentation. + for (const v of CLAUDE_ENV_CATALOG) { + assert.equal((v as Record).value, undefined) + assert.equal((v as Record).default, undefined) + } +}) diff --git a/web/src/App.tsx b/web/src/App.tsx index b2e4d33..e7fb1d4 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -8,8 +8,9 @@ import { AgentProfiles } from './routes/AgentProfiles' import { Providers } from './routes/Providers' import { Settings } from './routes/Settings' import { Doctor } from './routes/Doctor' +import { Environment } from './routes/Environment' -type Tab = 'profiles' | 'accounts' | 'agentProfiles' | 'providers' | 'doctor' | 'settings' +type Tab = 'profiles' | 'accounts' | 'agentProfiles' | 'providers' | 'environment' | 'doctor' | 'settings' const TABS: { id: Tab; label: string }[] = [ // Ordered as the concepts compose: who pays, what runs, then the pairing. @@ -17,6 +18,7 @@ const TABS: { id: Tab; label: string }[] = [ { id: 'agentProfiles', label: 'Agent profiles' }, { id: 'profiles', label: 'Profiles' }, { id: 'providers', label: 'Providers' }, + { id: 'environment', label: 'Environment' }, { id: 'doctor', label: 'Doctor' }, { id: 'settings', label: 'Settings' }, ] @@ -145,6 +147,7 @@ export function App() { {tab === 'agentProfiles' ? : null} {tab === 'profiles' ? : null} {tab === 'providers' ? : null} + {tab === 'environment' ? : null} {tab === 'doctor' ? : null} {tab === 'settings' ? : null} diff --git a/web/src/api.ts b/web/src/api.ts index ab4c1c3..185daba 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -155,6 +155,23 @@ export type MeasuredAccount = { sevenDay: UsageWindow | null } +/** How much is actually known about a variable. See the catalog's header. */ +export type ClaudeEnvKind = 'documented' | 'undocumented' | 'internal' + +export type ClaudeEnvVar = { + name: string + kind: ClaudeEnvKind + why?: string + category?: string + description?: string + managed?: boolean +} + +export type ClaudeEnvCatalog = { + source: { agent: string; version: string } + variables: ClaudeEnvVar[] +} + export type UsageReport = { accounts: MeasuredAccount[] checkedAt: number | null @@ -303,6 +320,14 @@ export const api = { catalog: (id: string) => call(`/api/catalog/${encodeURIComponent(id)}`), + /** + * Every environment variable Claude Code references. + * + * Fetched on demand, not in `bootstrap`: ~57 kB that only the Environment + * screen wants, and a cold start should not pay for a screen nobody opened. + */ + claudeEnv: () => call('/api/claude-env'), + /** * Measure every account's remaining subscription window, and cache it. * diff --git a/web/src/routes/Environment.tsx b/web/src/routes/Environment.tsx new file mode 100644 index 0000000..142f66e --- /dev/null +++ b/web/src/routes/Environment.tsx @@ -0,0 +1,219 @@ +import { useEffect, useMemo, useState } from 'react' +import { css } from '../../styled-system/css' +import { ApiError, api, type ClaudeEnvCatalog, type ClaudeEnvKind, type ClaudeEnvVar } from '../api' +import { Banner, Button, Empty, Panel, inputStyle } from '../ui' + +/** + * Every environment variable Claude Code references. + * + * THE CLASSIFICATION IS THE FEATURE, not a caveat bolted onto it. The list is + * extracted from the shipped binary, which yields names and nothing else — no + * meaning, no defaults, no proof a name is still wired to anything. So the + * screen leads with how much is known about each entry rather than presenting + * 495 strings as if they were equally supported knobs: + * + * documented described by hand, safe to act on + * undocumented the name is real, the meaning is not known + * internal test hooks, profiling, cloud auth, unreleased codenames + * + * A screen that hid that distinction would be actively harmful: someone would + * set an unreleased feature codename in a profile and wonder why nothing + * happened — or why it stopped working after an agent update. + */ + +const KIND_COPY: Record = { + documented: { + label: 'Documented', + blurb: 'Described from Anthropic’s docs, `claude --help`, or swisscode’s own adapter. Safe to act on.', + tone: 'ok', + }, + undocumented: { + label: 'Undocumented', + blurb: + 'The name is real — Claude Code references it. What it does is NOT known. Deliberately shipped without a description: a plausible guess is worse than a blank, because someone acts on it.', + tone: 'warn', + }, + internal: { + label: 'Internal', + blurb: + 'Test hooks, profiling switches, third-party cloud auth, and unreleased feature codenames. Listed for completeness; not knobs to set.', + tone: 'faint', + }, +} + +export function Environment() { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const [query, setQuery] = useState('') + const [kinds, setKinds] = useState>(new Set(['documented'])) + + useEffect(() => { + let live = true + api + .claudeEnv() + .then((r) => live && setData(r)) + .catch((err) => live && setError(err instanceof ApiError ? err.message : String(err))) + return () => { + live = false + } + }, []) + + const rows = useMemo(() => { + if (!data) return [] + const terms = query.toLowerCase().split(/\s+/).filter(Boolean) + return data.variables.filter((v) => { + if (!kinds.has(v.kind)) return false + if (terms.length === 0) return true + // Search the description too — "how do I cap output tokens" is a likelier + // question than "what does MAX_OUTPUT_TOKENS do". + const hay = `${v.name} ${v.category ?? ''} ${v.description ?? ''}`.toLowerCase() + return terms.every((t) => hay.includes(t)) + }) + }, [data, query, kinds]) + + const counts = useMemo(() => { + const c: Record = { documented: 0, undocumented: 0, internal: 0 } + for (const v of data?.variables ?? []) c[v.kind] = (c[v.kind] ?? 0) + 1 + return c + }, [data]) + + const toggle = (kind: ClaudeEnvKind) => + setKinds((s) => { + const next = new Set(s) + if (next.has(kind)) next.delete(kind) + else next.add(kind) + return next + }) + + return ( + <> +
+

Environment

+ {data ? ( + + {data.variables.length} variables · extracted from {data.source.agent}{' '} + {data.source.version} + + ) : null} +
+ +

+ Every environment variable this Claude Code build references. Set any of them on a profile’s{' '} + env block, which is applied last and can override anything the provider sets.{' '} + The list is extracted from the binary, so + it is complete on names and silent on meaning — which is what the filters below are for. +

+ + {error ? {error} : null} + +
+ setQuery(e.target.value)} + /> +
+ +
+ {(Object.keys(KIND_COPY) as ClaudeEnvKind[]).map((kind) => ( + + ))} +
+ + {[...kinds].map((kind) => ( +

+ + {KIND_COPY[kind].label}: + {' '} + {KIND_COPY[kind].blurb} +

+ ))} + + + {!data && !error ? Loading… : null} + {data && rows.length === 0 ? ( + Nothing matches. Try a different search, or enable another category above. + ) : null} + {rows.map((v) => ( + + ))} + + + ) +} + +function Row({ variable }: { variable: ClaudeEnvVar }) { + const [copied, setCopied] = useState(false) + return ( +
+
+ + {variable.name} + + {/* + "swisscode sets this" is the most useful badge on the screen: it tells + someone the knob is already wired to a profile field, so hand-setting + it in `env` would fight the launcher rather than configure it. + */} + {variable.managed ? ( + swisscode sets this + ) : null} + {variable.category ? ( + + {variable.category} + + ) : null} + {variable.kind === 'internal' ? ( + {variable.why} + ) : null} + +
+ {variable.description ? ( +
+ {variable.description} +
+ ) : ( + /* + An explicit blank, not an empty cell. The absence is the finding: we + know the name exists and we do not know what it does, and saying so is + the honest version of a catalog built by reading a binary. + */ +
+ {variable.kind === 'internal' + ? 'Not a user-facing setting.' + : 'No description — swisscode does not know what this does.'} +
+ )} +
+ ) +} From 74468e1e9eb8ca3af8f3fba8b69ed44a5f5f5e93 Mon Sep 17 00:00:00 2001 From: jellologic <31935831+jellologic@users.noreply.github.com> Date: Thu, 23 Jul 2026 07:03:13 -0400 Subject: [PATCH 2/3] feat(web): a design system with light/dark, enforced by the type checker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI had a dark-only palette of raw hex, twelve distinct hardcoded font sizes, and no scale for anything. This replaces it with a token system, and makes the tokens mandatory. MEASURED, NOT INVENTED. The type scale, weights, surface ramp and motion are read out of Linear's shipped CSS custom properties on the live site via Playwright; the light ramp and the tracking curve out of Apple's. Two things they agree on, and they matter more than any single colour: - Negative tracking that scales with size — Apple runs -0.01em at 12px and -0.022em at 17px. Large text at default tracking is the most common tell of an unstyled app, so tracking ships inside each textStyle rather than being left to call sites to remember. - Hairlines, not shadows. Linear's cards are a 12px radius and a 1px translucent border with `box-shadow: none`. Shadows exist as tokens and are used nowhere. Specifics worth keeping: weight 590 for titles (Linear's, and visibly less heavy-handed than semibold on a variable font), 32px controls, 0.1s transitions, #1d1d1f/#f7f8f8 for text because neither reference ships pure black or white, and #f5f5f7 for the light canvas so a white panel has something to sit on. `strictTokens` IS THE POINT. `color: '#fff'` and `fontSize: '13px'` are now type errors: 241 of them when the rule went on, now zero. A system nothing enforces decays into arbitrary values within a release — this codebase proved that before the rule existed. Genuinely arbitrary layout values remain legal through Panda's `[...]` escape, which keeps them visible as deliberate choices; there are 26, all dimensions. LIGHT/DARK FOLLOWS THE SYSTEM, with an override that does not defeat it. `data-theme` on always holds a RESOLVED value, so components style against one condition instead of reasoning about a three-state preference. `system` is the default and a real choice: a live `matchMedia` listener means the OS switching at sunset re-resolves immediately. Status colours are darker in light mode — reusing the dark-mode green and amber on white is the usual way a light theme ends up illegible. Two problems found by building it rather than by reading it: - THE CSP BLOCKED THE THEME SCRIPT. Resolving the theme before first paint needs an inline script, and `script-src 'self'` refused it — so the theme applied late, via React, which is exactly the white flash the inline script exists to prevent. Rather than add `unsafe-inline`, the document's CSP is now DERIVED FROM THE DOCUMENT: the exact bytes are hashed and named. That is tighter than before, not looser — an injected script still cannot run, and tampering invalidates the hash. - THE STATUS DOT RENDERED TRANSPARENT, and typechecked perfectly while doing it. `css({ bg: LOOKUP[tone] })` is a runtime value, and Panda is a build-time extractor: it emits classes for the literals it can see in source, so a lookup produces no CSS at all. Dot and Badge are `cva` recipes now, where the variants are literal at build time and the prop stays typed. Worth stating plainly: type safety did not catch this one, and could not have. 782 tests, 134.4 kB packed. Verified in a browser in both modes: tokens switching, the dot picking #1a7f37 on light and #3fb950 on dark, and no console errors under the derived CSP. Signed-off-by: jellologic <31935831+jellologic@users.noreply.github.com> --- src/adapters/web/security.ts | 48 +++++ src/adapters/web/server.ts | 19 +- test/adapters/web.test.ts | 41 +++++ web/index.html | 25 +++ web/panda.config.ts | 237 ++++++++++++++++++++---- web/src/App.tsx | 119 +++++++++--- web/src/index.css | 48 ++++- web/src/routes/Accounts.tsx | 24 +-- web/src/routes/AgentProfiles.tsx | 32 ++-- web/src/routes/Doctor.tsx | 29 +-- web/src/routes/Environment.tsx | 39 ++-- web/src/routes/ModelPicker.tsx | 52 +++--- web/src/routes/Profiles.tsx | 28 +-- web/src/routes/Providers.tsx | 32 ++-- web/src/routes/Settings.tsx | 8 +- web/src/theme.ts | 69 +++++++ web/src/ui.tsx | 300 +++++++++++++++++++++---------- 17 files changed, 877 insertions(+), 273 deletions(-) create mode 100644 web/src/theme.ts diff --git a/src/adapters/web/security.ts b/src/adapters/web/security.ts index 74f24ca..729cea8 100644 --- a/src/adapters/web/security.ts +++ b/src/adapters/web/security.ts @@ -20,6 +20,8 @@ * the attacker's page is forbidden by the same-origin policy from reading our * response body to learn it. */ +import { createHash } from 'node:crypto' + export type WebSecurityOptions = { token: string /** the port the server actually bound, needed to validate Host exactly */ @@ -168,6 +170,52 @@ export function guardDocumentRequest( * `Cache-Control: no-store` because the document carries the session token, and * a token in a disk cache outlives the server that issued it. */ +/** + * The CSP for a document, with the hashes of the inline scripts it contains. + * + * WHY HASHES AND NOT `unsafe-inline`. The page needs exactly one inline script: + * the theme resolver, which must run BEFORE first paint or every dark-mode user + * gets a white flash on load. Deferring it to React is what causes that flash, + * and moving it to a file makes the document depend on a second request to look + * right. So it stays inline — and the CSP names its exact bytes. + * + * This is TIGHTER than it looks, not a loosening. `unsafe-inline` would allow + * any injected script to run; a hash allows one specific script and nothing + * else, so an attacker who manages to inject a `' + const csp = documentSecurityHeaders(html)['content-security-policy']! + assert.match(csp, /script-src 'self' 'sha256-[A-Za-z0-9+/=]+'/) + assert.doesNotMatch(csp, /unsafe-inline[^;]*script/) + // Everything else about the policy is untouched. + assert.match(csp, /default-src 'self'/) + assert.match(csp, /frame-ancestors 'none'/) +}) + +test('a different script body produces a different hash — tampering breaks it', () => { + // The property that makes this safe: the hash is computed from the document + // actually being served, so an injected script cannot match it. + const a = documentSecurityHeaders('')['content-security-policy']! + const b = documentSecurityHeaders('')['content-security-policy']! + assert.notEqual(a, b) +}) + +test('external scripts are governed by \'self\' and are not hashed', () => { + // Hashing a `src` script would be meaningless — the hash applies to inline + // bodies. The bundle is loaded this way and must stay covered by 'self'. + assert.deepEqual(inlineScriptHashes(''), []) + assert.deepEqual(inlineScriptHashes(''), []) + assert.equal(inlineScriptHashes('').length, 0, 'an empty script needs no hash') +}) + +test('a document with no inline script gets the plain strict policy', () => { + const csp = documentSecurityHeaders('')['content-security-policy']! + assert.match(csp, /script-src 'self';/) + assert.doesNotMatch(csp, /sha256/) +}) diff --git a/web/index.html b/web/index.html index 881aa87..4b1a57d 100644 --- a/web/index.html +++ b/web/index.html @@ -11,6 +11,31 @@ cannot be read by anyone else. --> + +
diff --git a/web/panda.config.ts b/web/panda.config.ts index 14ac4bc..ff7de59 100644 --- a/web/panda.config.ts +++ b/web/panda.config.ts @@ -1,63 +1,230 @@ import { defineConfig } from '@pandacss/dev' /** + * The design system. + * * Panda is BUILD-TIME ONLY: it emits static CSS and ships no runtime library, - * which is why a styling system could be added to a project whose whole pitch - * is four runtime dependencies without changing that number. + * which is why a styling system exists in a project whose whole pitch is four + * runtime dependencies without changing that number. + * + * WHERE THE NUMBERS COME FROM. They are measured, not invented. The type scale, + * weights, surface ramp and motion are read out of Linear's shipped CSS custom + * properties on the live site; the light ramp and the tracking curve are read + * out of Apple's. Two things they agree on, which is what makes a UI read as + * "clean" far more than any individual colour: + * + * 1. NEGATIVE TRACKING THAT SCALES WITH SIZE. Apple runs -0.01em at 12px and + * -0.022em at 17px; Linear's display sizes sit around -0.022em. Large text + * left at default tracking is the most common tell of an unstyled app. + * 2. HAIRLINES, NOT SHADOWS, for structure. Linear's cards are a 12px radius + * and a 1px translucent border with `box-shadow: none`. Shadows are kept + * for things that genuinely float. * - * The token set below is the "Linear" look, and it is mostly restraint: a - * near-black surface ramp, hairline borders doing the structural work instead - * of shadows, a three-step text hierarchy, and colour reserved for status. + * `strictTokens` IS THE POINT OF THIS FILE. With it on, `color: '#fff'` and + * `fontSize: '13px'` are TYPE ERRORS: every value in the app must name a token. + * That is what makes this a design system rather than a palette some components + * happen to reference — one that nothing enforces decays into arbitrary values + * within a release, and this codebase already carried twelve distinct hardcoded + * font sizes before the rule existed. */ export default defineConfig({ preflight: true, include: ['./src/**/*.{ts,tsx}'], exclude: [], - // No dark VARIANT: this UI is dark, full stop. A theme toggle nobody asked - // for is two code paths to keep honest. + + // The whole reason this config is worth having. See the note above. + strictTokens: true, + + /** + * `data-theme` is stamped on and always holds a RESOLVED value — + * `light` or `dark`, never `system`. Resolution happens once, before first + * paint, so there is no flash of the wrong theme and no component ever has to + * ask which mode it is in. + */ + conditions: { + extend: { + dark: '[data-theme=dark] &', + light: '[data-theme=light] &', + }, + }, + theme: { extend: { tokens: { - colors: { - // surfaces, darkest first - bg: { value: '#0c0d10' }, - panel: { value: '#101116' }, - raised: { value: '#16181d' }, - hover: { value: '#1b1e24' }, - // hairlines - line: { value: '#22252c' }, - lineStrong: { value: '#2e323b' }, - // text, three steps and no more - text: { value: '#e7e8ea' }, - dim: { value: '#9ba1ac' }, - faint: { value: '#6b7280' }, - // status, used sparingly - accent: { value: '#5e6ad2' }, - accentHover: { value: '#6e79db' }, - ok: { value: '#3fb950' }, - warn: { value: '#d29922' }, - danger: { value: '#f85149' }, - }, - radii: { - sm: { value: '4px' }, - md: { value: '6px' }, - lg: { value: '8px' }, - }, fonts: { + // No webfont is loaded: a launcher's local config UI must not block + // paint on a network request, so this uses what the machine has. + // "Inter Variable" is named first for parity with Linear when present. sans: { value: - 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif', + 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter Variable", "Inter", "SF Pro Text", "Segoe UI", Roboto, sans-serif', + }, + mono: { + value: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace', }, - mono: { value: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace' }, + }, + fontWeights: { + // 590 rather than 600: it is what Linear's titles use, and on a + // variable font it reads as a heading without the heavy-handedness of + // semibold. Static fonts round it to the nearest weight they have. + normal: { value: '400' }, + medium: { value: '510' }, + title: { value: '590' }, + }, + radii: { + xs: { value: '4px' }, + sm: { value: '6px' }, + md: { value: '8px' }, + lg: { value: '12px' }, + full: { value: '9999px' }, + }, + // Linear's three, verbatim. Used almost nowhere, by design. + shadows: { + low: { value: '0 2px 4px rgba(0,0,0,0.10)' }, + medium: { value: '0 4px 24px rgba(0,0,0,0.20)' }, + high: { value: '0 7px 32px rgba(0,0,0,0.35)' }, + }, + durations: { + // 0.1s is Linear's interaction speed: fast enough to read as a state + // change rather than an animation. + fast: { value: '0.1s' }, + slow: { value: '0.15s' }, + }, + sizes: { + // One control height for the whole app. Linear's is 32px. + control: { value: '32px' }, + controlSm: { value: '26px' }, + sidebar: { value: '208px' }, + content: { value: '46rem' }, }, }, + + /** + * Every colour the app may use, in both modes. + * + * Components name a ROLE — `surface.panel`, `content.secondary` — never a + * shade. That is what makes light mode a data change rather than a second + * set of components, and it is what lets `strictTokens` be on at all. + */ semanticTokens: { colors: { - border: { value: '{colors.line}' }, + surface: { + // Apple's off-white, not #fff: a pure-white canvas behind a panel + // that is also white leaves nothing to see. + canvas: { value: { base: '#f5f5f7', _dark: '#08090a' } }, + panel: { value: { base: '#ffffff', _dark: '#101112' } }, + raised: { value: { base: '#ffffff', _dark: '#1c1c1f' } }, + hover: { value: { base: '#f0f0f3', _dark: '#232326' } }, + active: { value: { base: '#e8e8ed', _dark: '#28282c' } }, + overlay: { value: { base: 'rgba(0,0,0,0.25)', _dark: 'rgba(0,0,0,0.55)' } }, + }, + content: { + // #1d1d1f is Apple's near-black, #f7f8f8 Linear's near-white. + // Neither ships pure black or pure white as text. + primary: { value: { base: '#1d1d1f', _dark: '#f7f8f8' } }, + secondary: { value: { base: '#515154', _dark: '#d0d6e0' } }, + tertiary: { value: { base: '#86868b', _dark: '#8a8f98' } }, + inverse: { value: { base: '#ffffff', _dark: '#08090a' } }, + }, + border: { + subtle: { value: { base: 'rgba(0,0,0,0.06)', _dark: 'rgba(255,255,255,0.06)' } }, + default: { value: { base: 'rgba(0,0,0,0.10)', _dark: '#23252a' } }, + strong: { value: { base: 'rgba(0,0,0,0.18)', _dark: '#31333a' } }, + }, + accent: { + default: { value: { base: '#5e6ad2', _dark: '#7b86e8' } }, + hover: { value: { base: '#4f5bc4', _dark: '#8d97ee' } }, + subtle: { value: { base: 'rgba(94,106,210,0.10)', _dark: 'rgba(123,134,232,0.14)' } }, + }, + // Status colours are DARKER in light mode. The dark-mode green and + // amber are picked against near-black and fail contrast on white; + // reusing them is the usual way a light theme ends up illegible. + ok: { + default: { value: { base: '#1a7f37', _dark: '#3fb950' } }, + subtle: { value: { base: 'rgba(26,127,55,0.10)', _dark: 'rgba(63,185,80,0.14)' } }, + }, + warn: { + default: { value: { base: '#9a6700', _dark: '#d29922' } }, + subtle: { value: { base: 'rgba(154,103,0,0.10)', _dark: 'rgba(210,153,34,0.14)' } }, + }, + danger: { + default: { value: { base: '#cf222e', _dark: '#f85149' } }, + subtle: { value: { base: 'rgba(207,34,46,0.10)', _dark: 'rgba(248,81,73,0.14)' } }, + }, + }, + }, + + /** + * The type scale: Linear's sizes with Apple's tracking curve applied. + * + * These are `textStyles` rather than loose font-size tokens because a + * size without its line-height and tracking is exactly the part that gets + * forgotten, and the tracking is half of why this looks the way it does. + */ + textStyles: { + display: { + value: { + fontSize: '28px', + lineHeight: '1.15', + fontWeight: '590', + letterSpacing: '-0.022em', + }, + }, + title: { + value: { + fontSize: '20px', + lineHeight: '1.3', + fontWeight: '590', + letterSpacing: '-0.018em', + }, + }, + heading: { + value: { + fontSize: '15px', + lineHeight: '1.4', + fontWeight: '590', + letterSpacing: '-0.012em', + }, + }, + body: { + value: { + fontSize: '13px', + lineHeight: '1.5', + fontWeight: '400', + letterSpacing: '-0.006em', + }, + }, + // The workhorse: every secondary line, badge and hint in the app. + meta: { + value: { + fontSize: '12px', + lineHeight: '1.45', + fontWeight: '400', + letterSpacing: '-0.003em', + }, + }, + micro: { + value: { + fontSize: '11px', + lineHeight: '1.4', + fontWeight: '400', + letterSpacing: '0', + }, + }, + // Monospace never gets negative tracking: it is set to be aligned and + // counted, and tightening it defeats the reason to reach for it. + code: { + value: { + fontFamily: 'mono', + fontSize: '12px', + lineHeight: '1.5', + letterSpacing: '0', + }, }, }, }, }, + outdir: 'styled-system', jsxFramework: 'react', }) diff --git a/web/src/App.tsx b/web/src/App.tsx index e7fb1d4..78b82e3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,12 @@ import { useCallback, useEffect, useState } from 'react' import { css } from '../styled-system/css' +import { + applyTheme, + readPreference, + resolveTheme, + writePreference, + type ThemePreference, +} from './theme' import { ApiError, api, type Bootstrap } from './api' import { Banner, Dot } from './ui' import { Profiles } from './routes/Profiles' @@ -23,6 +30,73 @@ const TABS: { id: Tab; label: string }[] = [ { id: 'settings', label: 'Settings' }, ] +/** + * Light, dark, or follow the machine. + * + * `system` is the DEFAULT AND A REAL CHOICE, not the absence of one — picking + * light at noon should not mean the app ignores the machine switching to dark + * at sunset. The live listener below is what makes that true: while the + * preference is `system`, the OS changing re-resolves immediately, with no + * reload. + */ +function ThemeControl() { + const [preference, setPreference] = useState(() => readPreference()) + + useEffect(() => { + applyTheme(resolveTheme(preference)) + writePreference(preference) + if (preference !== 'system') return + const media = matchMedia('(prefers-color-scheme: dark)') + const onChange = () => applyTheme(resolveTheme('system')) + media.addEventListener('change', onChange) + return () => media.removeEventListener('change', onChange) + }, [preference]) + + const OPTIONS: readonly { id: ThemePreference; label: string }[] = [ + { id: 'system', label: 'Auto' }, + { id: 'light', label: 'Light' }, + { id: 'dark', label: 'Dark' }, + ] + + return ( +
+
Theme
+
+ {OPTIONS.map((o) => ( + + ))} +
+
+ ) +} + export function App() { const [data, setData] = useState(null) const [error, setError] = useState(null) @@ -43,36 +117,36 @@ export function App() { if (error) { return ( -
+
Could not reach swisscode: {error}
) } if (!data) { - return
loading…
+ return
loading…
} const installed = data.installedAgents ?? [] return ( -
+
-
+
{data.readOnly ? ( config.json is a newer schema than this swisscode understands. Every write is diff --git a/web/src/index.css b/web/src/index.css index 9ff17a0..ada256a 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,12 +1,54 @@ @layer reset, base, tokens, recipes, utilities; +/* + * The base layer only does what a token cannot: page-level typography, the + * scrollbar, and the focus ring. Everything with a colour names a semantic + * token, so light mode needs nothing here. + */ html, body, #root { height: 100% } + body { margin: 0; - background: var(--colors-bg); - color: var(--colors-text); + background: var(--colors-surface-canvas); + color: var(--colors-content-primary); font-family: var(--fonts-sans); font-size: 13px; + line-height: 1.5; + letter-spacing: -0.006em; + /* Antialiasing matters most in dark mode, where unhinted text looks heavy. */ -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +input, select, button, textarea { font-family: inherit; font-size: inherit } + +/* + * ONE focus ring for the whole app, and only for keyboard users. `:focus-visible` + * rather than `:focus` is what stops a ring appearing on every mouse click while + * keeping it for anyone navigating by keyboard. + */ +:focus-visible { + outline: 2px solid var(--colors-accent-default); + outline-offset: 2px; + border-radius: var(--radii-xs); +} + +/* Match the scrollbar to the theme; an unstyled one is a white gutter in dark mode. */ +* { scrollbar-color: var(--colors-border-strong) transparent } +::-webkit-scrollbar { width: 10px; height: 10px } +::-webkit-scrollbar-thumb { + background: var(--colors-border-strong); + border-radius: var(--radii-full); + border: 3px solid transparent; + background-clip: content-box; +} +::-webkit-scrollbar-thumb:hover { background-clip: content-box; background-color: var(--colors-content-tertiary) } +::-webkit-scrollbar-track { background: transparent } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } } -input, select, button { font-family: inherit } diff --git a/web/src/routes/Accounts.tsx b/web/src/routes/Accounts.tsx index 234b8ca..a78af60 100644 --- a/web/src/routes/Accounts.tsx +++ b/web/src/routes/Accounts.tsx @@ -146,7 +146,7 @@ export function Accounts({ data, reload }: { data: Bootstrap; reload: () => Prom <>
-

+

{isNew ? 'New account' : `Account · ${editing}`}

@@ -237,7 +237,7 @@ export function Accounts({ data, reload }: { data: Bootstrap; reload: () => Prom directory.
{!isNew && data.logins ? ( -
+
currently: {data.logins[editing] ?? 'not logged in'}
) : null} @@ -287,7 +287,7 @@ export function Accounts({ data, reload }: { data: Bootstrap; reload: () => Prom return ( <>
-

Accounts

+

Accounts

{/* Measuring is a BUTTON rather than something the page does on load. @@ -316,7 +316,7 @@ export function Accounts({ data, reload }: { data: Bootstrap; reload: () => Prom ) : null} {usage?.checkedAt ? ( -
+
Measured {new Date(usage.checkedAt).toLocaleTimeString()}. “% left” is the tighter of the two windows, never their average — an account at 5% of its 5-hour window and 95% of its weekly one has almost nothing left. Profiles using the usage strategy now select on @@ -343,8 +343,8 @@ export function Accounts({ data, reload }: { data: Bootstrap; reload: () => Prom alignItems: 'center', gap: '3', py: '2.5', - borderBottom: '1px solid', - borderColor: 'line', + borderBottom: '[1px solid]', + borderColor: 'border.subtle', _last: { borderBottom: 'none' }, })} > @@ -354,17 +354,17 @@ export function Accounts({ data, reload }: { data: Bootstrap; reload: () => Prom looks identical in config.json and fails only after execve, so the dot tracks the login rather than the field. */} - -
-
+ +
+
{name} {a.label ? ( - + {a.label} ) : null}
-
+
{a.provider} {' · '} {credentialLine(a, login)} @@ -372,7 +372,7 @@ export function Accounts({ data, reload }: { data: Bootstrap; reload: () => Prom {usedBy.length > 0 ? `used by ${usedBy.join(', ')}` : 'unused'}
{measured ? ( -
+
{measured.mode === 'key' ? 'key account — no subscription window' : measured.remaining === null diff --git a/web/src/routes/AgentProfiles.tsx b/web/src/routes/AgentProfiles.tsx index 54ff3d0..48867a8 100644 --- a/web/src/routes/AgentProfiles.tsx +++ b/web/src/routes/AgentProfiles.tsx @@ -85,7 +85,7 @@ export function AgentProfiles({ data, reload }: { data: Bootstrap; reload: () => <>
-

+

{isNew ? 'New agent profile' : `Agent profile · ${editing}`}

@@ -134,7 +134,7 @@ export function AgentProfiles({ data, reload }: { data: Bootstrap; reload: () => -

+

All four tiers, from one table. Claude Code reads the extended-context marker per variable, so a tier left out is the bug where three run wide and the fourth silently does not. Blank inherits the provider default. @@ -155,7 +155,7 @@ export function AgentProfiles({ data, reload }: { data: Bootstrap; reload: () => ))} {!provider ? ( -

+

No profile uses this setup yet, so there is no provider to browse a catalog from. Type ids by hand, or attach it to a profile first.

@@ -189,7 +189,7 @@ export function AgentProfiles({ data, reload }: { data: Bootstrap; reload: () =>
-