diff --git a/src-tauri/src/commands/claude_cli.rs b/src-tauri/src/commands/claude_cli.rs index 895fbbd0e..862bd316a 100644 --- a/src-tauri/src/commands/claude_cli.rs +++ b/src-tauri/src/commands/claude_cli.rs @@ -115,6 +115,13 @@ fn claude_content_blocks(content: &ClaudeContent) -> Vec { } } +/// Expand a leading `~/` to the user's home directory; leave other paths unchanged. +fn expand_path(raw: &str) -> PathBuf { + raw.strip_prefix("~/") + .and_then(|rest| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(rest))) + .unwrap_or_else(|| PathBuf::from(raw)) +} + async fn find_claude_command() -> Result { find_cli_command("claude", &["claude.cmd", "claude.exe"]).await } @@ -213,6 +220,7 @@ pub async fn claude_cli_spawn( messages: Vec, isolate_local_config: bool, working_directory: Option, + claude_config_dir: Option, ) -> Result<(), String> { // Build the turn list: fold any system messages into a preamble on // the first user turn rather than using a CLI flag, because @@ -258,6 +266,9 @@ pub async fn claude_cli_spawn( suppress_windows_console(&mut cmd); cmd.args(build_claude_cli_args(&model, isolate_local_config)); cmd.current_dir(&working_directory); + if let Some(dir) = claude_config_dir.as_deref().map(str::trim).filter(|s| !s.is_empty()) { + cmd.env("CLAUDE_CONFIG_DIR", expand_path(dir)); + } cmd.stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -594,4 +605,20 @@ mod tests { std::fs::remove_dir_all(&dir).expect("cleanup temp dir"); } + + #[test] + fn expand_path_expands_tilde_slash_to_home_directory() { + let home = std::env::var_os("HOME").expect("HOME must be set"); + assert_eq!(expand_path("~/.claude-personal"), PathBuf::from(home).join(".claude-personal")); + } + + #[test] + fn expand_path_leaves_absolute_path_unchanged() { + assert_eq!(expand_path("/home/user/.claude"), PathBuf::from("/home/user/.claude")); + } + + #[test] + fn expand_path_does_not_expand_bare_tilde_without_slash() { + assert_eq!(expand_path("~"), PathBuf::from("~")); + } } diff --git a/src/components/settings/preset-resolver.test.ts b/src/components/settings/preset-resolver.test.ts index 2c0dd2649..d3f29139d 100644 --- a/src/components/settings/preset-resolver.test.ts +++ b/src/components/settings/preset-resolver.test.ts @@ -156,6 +156,32 @@ describe("resolveConfig", () => { ).codexCliTimeoutMinutes).toBeUndefined() }) + it("carries claudeConfigDir only for the Claude Code preset", () => { + const claudePreset: LlmPreset = { + id: "claude-code-cli", + label: "Claude Code CLI", + provider: "claude-code", + defaultModel: "sonnet", + } + const codexPreset: LlmPreset = { + id: "codex-cli", + label: "Codex CLI", + provider: "codex-cli", + defaultModel: "gpt-5", + } + + expect(resolveConfig( + claudePreset, + { claudeConfigDir: "~/.claude-personal" }, + fallbackConfig(), + ).claudeConfigDir).toBe("~/.claude-personal") + expect(resolveConfig( + codexPreset, + { claudeConfigDir: "~/.claude-personal" }, + fallbackConfig(), + ).claudeConfigDir).toBeUndefined() + }) + it("does not apply local CLI isolation to hosted providers", () => { const preset: LlmPreset = { id: "openai", diff --git a/src/components/settings/preset-resolver.ts b/src/components/settings/preset-resolver.ts index ac894da7b..041c85642 100644 --- a/src/components/settings/preset-resolver.ts +++ b/src/components/settings/preset-resolver.ts @@ -80,6 +80,7 @@ export function resolveConfig( reasoning, localCliIsolation, codexCliTimeoutMinutes: preset.provider === "codex-cli" ? codexCliTimeoutMinutes : undefined, + claudeConfigDir: preset.provider === "claude-code" ? ov.claudeConfigDir : undefined, } } diff --git a/src/components/settings/sections/llm-provider-section.tsx b/src/components/settings/sections/llm-provider-section.tsx index 309938018..c603ff776 100644 --- a/src/components/settings/sections/llm-provider-section.tsx +++ b/src/components/settings/sections/llm-provider-section.tsx @@ -129,6 +129,7 @@ function PresetRow({ const context = ov.maxContextSize ?? preset.suggestedContextSize ?? 131072 const reasoning = ov.reasoning ?? { mode: "auto" as const } const localCliIsolation = ov.localCliIsolation === true + const claudeConfigDir = ov.claudeConfigDir ?? "" const codexCliTimeoutMinutes = Math.max(1, Math.min(240, ov.codexCliTimeoutMinutes ?? 10)) const isLocalCliProvider = preset.provider === "claude-code" || preset.provider === "codex-cli" const [testState, setTestState] = useState({ kind: "idle" }) @@ -354,6 +355,22 @@ function PresetRow({ )} + {preset.provider === "claude-code" && ( +
+ + onChange({ claudeConfigDir: e.target.value || undefined })} + className="font-mono text-xs" + /> +

+ {t("settings.sections.llm.claudeConfigDirHint")} +

+
+ )} + {preset.provider === "codex-cli" && (
diff --git a/src/i18n/en.json b/src/i18n/en.json index ef3945f3e..f3e80b2b9 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -305,6 +305,8 @@ "localCliIsolationHint": "Optional for Claude Code and Codex CLI. When enabled, LLM Wiki asks the CLI to ignore user-level rules/config/MCP/tools where supported. This is configuration isolation, not a process sandbox: the CLI still uses your local login credentials and network access. Leave off if you intentionally rely on your local CLI setup.", "localCliIsolationOn": "Isolation on: user CLI rules, MCP/tools, and persisted sessions are disabled where the CLI supports it. Local credentials and network access are still controlled by the CLI itself.", "localCliIsolationOff": "Isolation off: this provider can use your normal local CLI configuration.", + "claudeConfigDir": "Claude config directory", + "claudeConfigDirHint": "Injected as CLAUDE_CONFIG_DIR when spawning the CLI. Use this when your config lives outside the default ~/.claude location (e.g. ~/.claude-personal). Leave empty to use the CLI default.", "codexCliTimeout": "Codex CLI timeout", "codexCliTimeoutUnit": "minutes", "codexCliTimeoutHint": "Overall timeout for one Codex CLI request. Raise this for long PDF ingest or slower local/network sessions.", diff --git a/src/i18n/zh.json b/src/i18n/zh.json index d95a1440f..d7b9d2d2d 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -305,6 +305,8 @@ "localCliIsolationHint": "仅用于 Claude Code 和 Codex CLI。开启后,LLM Wiki 会让 CLI 尽量忽略用户级 rules / config / MCP / tools。这是配置隔离,不是进程沙箱:CLI 仍会使用你的本机登录凭据和网络能力。若你有意依赖本机 CLI 配置,请保持关闭。", "localCliIsolationOn": "隔离已开启:在 CLI 支持的范围内禁用用户规则、MCP/tools 和持久会话。本机凭据和网络访问仍由 CLI 自身控制。", "localCliIsolationOff": "隔离已关闭:该 Provider 可以使用你平时的本地 CLI 配置。", + "claudeConfigDir": "Claude 配置目录", + "claudeConfigDirHint": "启动 CLI 时将作为 CLAUDE_CONFIG_DIR 注入。适用于配置文件不在默认 ~/.claude 目录的情况(如 ~/.claude-personal)。留空则使用 CLI 默认路径。", "codexCliTimeout": "Codex CLI 超时时间", "codexCliTimeoutUnit": "分钟", "codexCliTimeoutHint": "单次 Codex CLI 请求的总超时时间。长 PDF 摄取或较慢的本地 / 网络会话可适当调大。", diff --git a/src/lib/claude-cli-transport.ts b/src/lib/claude-cli-transport.ts index a3bdc0772..bcba1ddc2 100644 --- a/src/lib/claude-cli-transport.ts +++ b/src/lib/claude-cli-transport.ts @@ -110,6 +110,7 @@ type SpawnPayload = Record & { messages: ChatMessage[] isolateLocalConfig: boolean workingDirectory: string + claudeConfigDir?: string } /** @@ -269,6 +270,7 @@ export async function streamClaudeCodeCli( messages, isolateLocalConfig: config.localCliIsolation === true, workingDirectory, + claudeConfigDir: config.claudeConfigDir, } await invoke("claude_cli_spawn", payload) if (aborted || signal?.aborted) { diff --git a/src/stores/wiki-store.ts b/src/stores/wiki-store.ts index a381495d1..8893ddc25 100644 --- a/src/stores/wiki-store.ts +++ b/src/stores/wiki-store.ts @@ -36,6 +36,12 @@ interface LlmConfig { localCliIsolation?: boolean /** Codex CLI provider only. Overall subprocess timeout in minutes. */ codexCliTimeoutMinutes?: number + /** + * Claude Code CLI only. Injected as CLAUDE_CONFIG_DIR when spawning the + * subprocess. Set this when the config lives outside the default ~/.claude + * location (e.g. ~/.claude-personal). Omit to let the CLI use its default. + */ + claudeConfigDir?: string } export type SearchProvider = "tavily" | "serpapi" | "searxng" | "ollama" | "firecrawl" | "none" @@ -286,6 +292,7 @@ export interface ProviderOverride { reasoning?: ReasoningConfig localCliIsolation?: boolean codexCliTimeoutMinutes?: number + claudeConfigDir?: string } export type ProviderConfigs = Record