feat(claude-code): route via ~/.claude/settings.json instead of a PATH shim#74
Merged
Merged
Conversation
…H shim Replace the Claude Code PATH-shim with Claude Code's native config: write `env.ANTHROPIC_BASE_URL` into ~/.claude/settings.json. This is the approach LiteLLM/Portkey use, it's read fresh on every `claude` invocation (no PATH race), and it sidesteps every shim failure mode (version pinning, PATH ordering, process-name identity guards). New: src/lib/claude-code-settings.ts — an ownership-guarded, atomic writer modeled on claude-desktop-config.ts. It owns EXACTLY ONE key, `ANTHROPIC_BASE_URL` (nested under settings.env); it never touches ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN or any other env var or setting. The ownership guard refuses to overwrite a foreign base URL (user-set or another gateway) and refuses to remove one on revert. Atomic O_EXCL + 0600 write; honors CLAUDE_CONFIG_DIR. Apps card: claude-code converts from "shimmable" to "config" (now a near-clone of Claude Desktop). `enabled` derives from isProxyBaseUrlConfigured(); toggle enable→applyProxyBaseUrl, disable→revertProxyBaseUrl. A foreign base URL surfaces as a new `conflict: "foreign-base-url"` field on the card. Detection is KEPT (drives "is claude installed?" → ready/not-installed + the install hint), but the per-binary version picker (installs[].active, selectedPath, the /claude-code/select route) is gone — claude reads its own settings regardless of which binary runs. Removed the entire shim apparatus from claude-cli-detect.ts (shim build/ install/remove, the precmd PATH-block machinery, getShimPath, etc.) plus tests/apps-shim.test.ts. KEPT detectClaudeInstalls + SHIM_MARKER + fileStartsWithContains: detection still skips a leftover legacy shim so it isn't mislabeled an install. uninstall now reverts the settings.json base URL (ownership-guarded) instead of deleting a shim; the first-launch installer PATH-block cleanup is unchanged. config: drop apps.claudeCode.selectedPath (zod nested-object strip drops it silently from old configs — no migration needed). settings-types: retire the "shimmable" kind and ClaudeCodeSelectRequest; AppInstall loses `active`; AppEntry gains `conflict`. Tests: new claude-code-settings.test.ts (29); apps-route + uninstall reworked for the settings.json model. Full suite 745 pass, check:fast + knip clean. Note: the Apps frontend (shell/src/features/apps) still references the retired shimmable kind / select endpoint and needs a follow-up change.
stuffbucket
added a commit
that referenced
this pull request
Jun 8, 2026
The settings.json pivot (#74) retired the per-binary shim model, but the Tauri shell's Apps card still spoke the old contract — kind:"shimmable", install.active, and a POST /claude-code/select call that now 404s — so the Claude Code toggle was wired to a backend that no longer answered. - api.ts: drop AppKind "shimmable" and AppInstall.active; remove the claude-code-select endpoint and the toggle's `path` param; add the `conflict` field to AppEntry. - AppCard.tsx: render Claude Code as a config card like Claude Desktop (install path in mono, single On/Off switch, no version radio picker). Add a conflict surface: when enabling is refused because the app already carries a base URL we don't own, show a plain-language callout naming the cause and the one fix — so the toggle snapping back is never a mystery dead-end. - useApps.ts / AppsPanel.tsx: drop selectClaudeCode and the path-threaded toggle. - styles.css: replace the dead .app-versions radio styles with the .app-card__conflict callout (destructive-tinted stripe, matches the requirement-callout idiom). Verified live against the new backend: list renders both Claude apps as config cards, the ownership guard preserves a foreign base URL on enable, and the conflict callout renders per the design system. Shell typechecks clean; settings UI builds.
stuffbucket
added a commit
that referenced
this pull request
Jun 8, 2026
) * feat(apps): lifecycle reconciler for Claude Code routing Decouple the routing *intent* (config.apps.claudeCode.enabled) from the on-disk *effect* (env.ANTHROPIC_BASE_URL in ~/.claude/settings.json) and reconcile at every proxy lifecycle edge, so a dead 127.0.0.1:4141 is never left stranded in Claude Code's config when maximal isn't running. - Boot: re-apply the base URL if routing is intended (self-heals a URL a prior crash/force-kill left over a dead proxy). - Graceful shutdown (SIGTERM/SIGINT/parent-death): remove the base URL so `claude` isn't stranded; intent flag persists, so next boot re-applies. - Crash (sidecar non-zero exit): the proxy can't revert itself, so the Tauri shell runs `maximal configure-claude-code --revert` on its behalf. All paths are ownership-guarded (a foreign ANTHROPIC_BASE_URL is never clobbered or removed) and only touch that one key — sibling env vars (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN) are untouched. Each death mode has exactly one reverter, so no double-revert races. New shared CLI subcommand `configure-claude-code [--revert]` mirrors configure-claude-desktop and works while the server is down. Reconcile helpers take injectable intent/path params (no mock.module) per CLAUDE.md. 16 new tests; full suite + cargo check green. * test(apps): fix configure-claude-code mock-bleed via injectable filePath apps-route.test.ts mocks claude-code-settings with path-defaulting wrappers; Bun persists that mock forward, so in CI's file ordering the subcommand's no-arg applyProxyBaseUrl() hit the mocked default path instead of the test's CLAUDE_CONFIG_DIR. Thread an injectable filePath through runConfigureClaudeCode and pass it explicitly in the test (CLAUDE.md: prefer injectable options over mock.module). Verified by running apps-route before configure-claude-code locally, reproducing the CI ordering. * feat(shell): rework Apps card for config-kind Claude Code routing The settings.json pivot (#74) retired the per-binary shim model, but the Tauri shell's Apps card still spoke the old contract — kind:"shimmable", install.active, and a POST /claude-code/select call that now 404s — so the Claude Code toggle was wired to a backend that no longer answered. - api.ts: drop AppKind "shimmable" and AppInstall.active; remove the claude-code-select endpoint and the toggle's `path` param; add the `conflict` field to AppEntry. - AppCard.tsx: render Claude Code as a config card like Claude Desktop (install path in mono, single On/Off switch, no version radio picker). Add a conflict surface: when enabling is refused because the app already carries a base URL we don't own, show a plain-language callout naming the cause and the one fix — so the toggle snapping back is never a mystery dead-end. - useApps.ts / AppsPanel.tsx: drop selectClaudeCode and the path-threaded toggle. - styles.css: replace the dead .app-versions radio styles with the .app-card__conflict callout (destructive-tinted stripe, matches the requirement-callout idiom). Verified live against the new backend: list renders both Claude apps as config cards, the ownership guard preserves a foreign base URL on enable, and the conflict callout renders per the design system. Shell typechecks clean; settings UI builds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the Claude Code PATH-shim with Claude Code's native config — writing
env.ANTHROPIC_BASE_URLinto~/.claude/settings.json. This is the approach LiteLLM / Portkey use; it's read fresh on everyclaudeinvocation (no PATH race), and it sidesteps every shim failure mode we hit (version pinning, PATH ordering losing the race, process-name identity guards that never matched).Why
Research (fanned out to subagents) confirmed: no one ships a binary-masquerade PATH shim — every gateway just sets
ANTHROPIC_BASE_URLor writes the config. Claude Code natively supports~/.claude/settings.jsonwith anenvblock. The shim was complex machinery for a problem a one-file write solves.What changed
New
src/lib/claude-code-settings.ts— ownership-guarded, atomic writer modeled onclaude-desktop-config.ts:ANTHROPIC_BASE_URL(nested undersettings.env). Never touchesANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKENor any other setting.O_EXCL+0600write; honorsCLAUDE_CONFIG_DIR.Apps card:
claude-codeconverts fromshimmable→config(now a near-clone of Claude Desktop).enabled←isProxyBaseUrlConfigured(); toggle enable→applyProxyBaseUrl, disable→revertProxyBaseUrl. A foreign base URL surfaces as a newconflict: "foreign-base-url"field. Detection is kept (drives "is claude installed?" → ready/not-installed + install hint); the per-binary version picker (installs[].active,selectedPath,/claude-code/select) is gone — claude reads its own settings regardless of which binary runs.Shim apparatus removed from
claude-cli-detect.ts(−408 lines) plustests/apps-shim.test.ts. KeptdetectClaudeInstalls+SHIM_MARKER+fileStartsWithContainsso detection still skips a leftover legacy shim rather than mislabeling it an install.uninstall now reverts the settings.json base URL (ownership-guarded) instead of deleting a shim; the first-launch installer PATH-block cleanup is unchanged.
config: drop
apps.claudeCode.selectedPath(zod nested-object strip drops it silently from old configs — no migration needed). settings-types: retire theshimmablekind +ClaudeCodeSelectRequest;AppInstalllosesactive;AppEntrygainsconflict.Verification
New
claude-code-settings.test.ts(29 cases: write/preserve-sibling-env, ownership guard, idempotency, revert variants, foreign-URL-left-intact, 0600,CLAUDE_CONFIG_DIR). apps-route + uninstall reworked. 745 pass / 0 fail, check:fast + knip clean, shell builds.Follow-ups (NOT in this PR)
shell/src/features/apps) still references the retiredshimmablekind //selectendpoint at runtime — it compiles (contract types are local) but needs the Apps card reworked to render Claude Code like Claude Desktop (drop the version picker, surfaceconflict)./status) and removing on proxy-down, via a sharedmaximal configure-claude-code [--revert]CLI subcommand so it works even when the server is down.