Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src-tauri/src/commands/claude_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ fn claude_content_blocks(content: &ClaudeContent) -> Vec<serde_json::Value> {
}
}

/// 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<PathBuf, String> {
find_cli_command("claude", &["claude.cmd", "claude.exe"]).await
}
Expand Down Expand Up @@ -213,6 +220,7 @@ pub async fn claude_cli_spawn(
messages: Vec<ClaudeMessage>,
isolate_local_config: bool,
working_directory: Option<String>,
claude_config_dir: Option<String>,
) -> 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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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("~"));
}
}
26 changes: 26 additions & 0 deletions src/components/settings/preset-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/components/settings/preset-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function resolveConfig(
reasoning,
localCliIsolation,
codexCliTimeoutMinutes: preset.provider === "codex-cli" ? codexCliTimeoutMinutes : undefined,
claudeConfigDir: preset.provider === "claude-code" ? ov.claudeConfigDir : undefined,
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/components/settings/sections/llm-provider-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderTestState>({ kind: "idle" })
Expand Down Expand Up @@ -354,6 +355,22 @@ function PresetRow({
</div>
)}

{preset.provider === "claude-code" && (
<div className="space-y-2 rounded-md border p-3">
<Label>{t("settings.sections.llm.claudeConfigDir")}</Label>
<Input
type="text"
placeholder="~/.claude"
value={claudeConfigDir}
onChange={(e) => onChange({ claudeConfigDir: e.target.value || undefined })}
className="font-mono text-xs"
/>
<p className="text-xs text-muted-foreground">
{t("settings.sections.llm.claudeConfigDirHint")}
</p>
</div>
)}

{preset.provider === "codex-cli" && (
<div className="space-y-2 rounded-md border p-3">
<Label>{t("settings.sections.llm.codexCliTimeout")}</Label>
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 摄取或较慢的本地 / 网络会话可适当调大。",
Expand Down
2 changes: 2 additions & 0 deletions src/lib/claude-cli-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type SpawnPayload = Record<string, unknown> & {
messages: ChatMessage[]
isolateLocalConfig: boolean
workingDirectory: string
claudeConfigDir?: string
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions src/stores/wiki-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -286,6 +292,7 @@ export interface ProviderOverride {
reasoning?: ReasoningConfig
localCliIsolation?: boolean
codexCliTimeoutMinutes?: number
claudeConfigDir?: string
}

export type ProviderConfigs = Record<string, ProviderOverride>
Expand Down