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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Only write entries that are worth mentioning to users.

## Unreleased

- Config: Auto-detect bash on Windows (Git Bash, MSYS2, Cygwin, WSL) and add `[shell]` configuration for explicit control

## 1.5 (2026-01-30)

- Web: Add Git diff status bar showing uncommitted changes in session working directory
Expand Down
34 changes: 34 additions & 0 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The configuration file contains the following top-level configuration items:
| `loop_control` | `table` | Agent loop control parameters |
| `services` | `table` | External service configuration (search, fetch) |
| `mcp` | `table` | MCP client configuration |
| `shell` | `table` | Shell configuration (Windows only) |

### Complete configuration example

Expand Down Expand Up @@ -156,6 +157,39 @@ When configuring the Kimi Code platform using the `/login` command, search and f
| --- | --- | --- | --- |
| `client.tool_call_timeout_ms` | `integer` | `60000` | MCP tool call timeout (milliseconds) |

### `shell`

::: warning
This configuration is **Windows only**. On Unix-like systems, the shell is always determined in this priority order: `config.path` → `SHELL` environment variable → auto-detection.
:::

`shell` configures the shell used for executing shell commands on Windows.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `path` | `string` | `null` | Explicit path to the shell executable. Highest priority. Example: `C:/Program Files/Git/bin/bash.exe` |
| `preferred` | `string` | `"auto"` | Preferred shell when path is not set. Options: `"auto"`, `"powershell"`, `"bash"` |

**`preferred` options:**

- `"auto"`: Check `SHELL` environment variable, then auto-detect bash from common locations (Git Bash, MSYS2, Cygwin), fallback to PowerShell
- `"powershell"`: Use PowerShell, ignoring `SHELL` environment variable
- `"bash"`: Auto-detect bash from common locations, ignoring `SHELL` environment variable

Example:

```toml
[shell]
path = "C:/Program Files/Git/bin/bash.exe"
```

Or use auto-detection:

```toml
[shell]
preferred = "bash" # Auto-detect bash, ignoring SHELL environment variable
```

## JSON configuration migration

If `~/.kimi/config.toml` doesn't exist but `~/.kimi/config.json` exists, Kimi Code CLI will automatically migrate the JSON configuration to TOML format and backup the original file as `config.json.bak`.
Expand Down
2 changes: 2 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This page documents the changes in each Kimi Code CLI release.

## Unreleased

- Config: Add `[shell]` configuration section for Windows users to use bash (Git Bash, MSYS2, Cygwin) instead of PowerShell

## 1.5 (2026-01-30)

- Web: Add Git diff status bar showing uncommitted changes in session working directory
Expand Down
34 changes: 34 additions & 0 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ kimi --config '{"default_model": "kimi-for-coding", "providers": {...}, "models"
| `loop_control` | `table` | Agent 循环控制参数 |
| `services` | `table` | 外部服务配置(搜索、抓取) |
| `mcp` | `table` | MCP 客户端配置 |
| `shell` | `table` | Shell 配置(仅 Windows) |

### 完整配置示例

Expand Down Expand Up @@ -156,6 +157,39 @@ capabilities = ["thinking", "image_in"]
| --- | --- | --- | --- |
| `client.tool_call_timeout_ms` | `integer` | `60000` | MCP 工具调用超时时间(毫秒) |

### `shell`

::: warning
此配置**仅适用于 Windows**。在类 Unix 系统上,Shell 始终按以下优先级确定:`config.path` → `SHELL` 环境变量 → 自动检测。
:::

`shell` 配置在 Windows 上执行 Shell 命令时使用的 Shell。

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `path` | `string` | `null` | Shell 可执行文件的显式路径。优先级最高。示例:`C:/Program Files/Git/bin/bash.exe` |
| `preferred` | `string` | `"auto"` | 未设置 path 时的首选 Shell。选项:`"auto"`、`"powershell"`、`"bash"` |

**`preferred` 选项说明:**

- `"auto"`:检查 `SHELL` 环境变量,然后从常见位置自动检测 bash(Git Bash、MSYS2、Cygwin),最后回退到 PowerShell
- `"powershell"`:使用 PowerShell,忽略 `SHELL` 环境变量
- `"bash"`:从常见位置自动检测 bash,忽略 `SHELL` 环境变量

示例:

```toml
[shell]
path = "C:/Program Files/Git/bin/bash.exe"
```

或使用自动检测:

```toml
[shell]
preferred = "bash" # 自动检测 bash,忽略 SHELL 环境变量
```

## JSON 配置迁移

如果 `~/.kimi/config.toml` 不存在但 `~/.kimi/config.json` 存在,Kimi Code CLI 会自动将 JSON 配置迁移到 TOML 格式,并将原文件备份为 `config.json.bak`。
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## 未发布

- Config:添加 `[shell]` 配置节,Windows 用户可使用 bash(Git Bash、MSYS2、Cygwin)替代 PowerShell

## 1.5 (2026-01-30)

- Web:添加 Git diff 状态栏,显示会话工作目录中的未提交更改
Expand Down
25 changes: 25 additions & 0 deletions src/kimi_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,28 @@ class MCPConfig(BaseModel):
)


class ShellConfig(BaseModel):
"""Shell configuration for command execution.

Note: The `preferred` option is Windows-only. On Unix-like systems,
the shell is always determined by: config.path > SHELL env var > auto-detect.
"""

path: str | None = Field(default=None, description="Explicit path to shell executable")
"""Explicit path to shell executable. Highest priority on all platforms.
Example: 'C:/Program Files/Git/bin/bash.exe'"""

preferred: Literal["auto", "powershell", "bash"] = Field(default="auto")
"""Preferred shell when path is not set (Windows only).
- "auto": Check SHELL env var, then auto-detect bash, then PowerShell fallback
- "powershell": Use PowerShell, bypass SHELL env var
- "bash": Auto-detect bash, bypass SHELL env var

On Unix-like systems, this option is ignored; shell selection follows
the standard priority: config.path > SHELL env var > auto-detect.
"""


class Config(BaseModel):
"""Main configuration structure."""

Expand All @@ -146,6 +168,8 @@ class Config(BaseModel):
loop_control: LoopControl = Field(default_factory=LoopControl, description="Agent loop control")
services: Services = Field(default_factory=Services, description="Services configuration")
mcp: MCPConfig = Field(default_factory=MCPConfig, description="MCP configuration")
shell: ShellConfig = Field(default_factory=ShellConfig, description="Shell configuration")
"""Shell configuration for command execution."""

@model_validator(mode="after")
def validate_model(self) -> Self:
Expand All @@ -169,6 +193,7 @@ def get_default_config() -> Config:
models={},
providers={},
services=Services(),
shell=ShellConfig(),
)


Expand Down
2 changes: 1 addition & 1 deletion src/kimi_cli/soul/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async def create(
ls_output, agents_md, environment = await asyncio.gather(
list_directory(session.work_dir),
load_agents_md(session.work_dir),
Environment.detect(),
Environment.detect(config.shell),
)

# Discover and format skills
Expand Down
4 changes: 3 additions & 1 deletion src/kimi_cli/tools/shell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ async def _read_stream(stream: AsyncReadable, cb: Callable[[bytes], None]):
raise

def _shell_args(self, command: str) -> tuple[str, ...]:
"""Generate shell arguments based on shell type."""
if self._is_powershell:
return (str(self._shell_path), "-command", command)
return (str(self._shell_path), "-Command", command)
# bash, zsh, fish, and sh all use -c flag
return (str(self._shell_path), "-c", command)
140 changes: 121 additions & 19 deletions src/kimi_cli/utils/environment.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
from __future__ import annotations

import os
import platform
from dataclasses import dataclass
from typing import Literal

from kaos.path import KaosPath

from kimi_cli.config import ShellConfig
from kimi_cli.utils.logging import logger


@dataclass(slots=True, frozen=True, kw_only=True)
class Environment:
os_kind: Literal["Windows", "Linux", "macOS"] | str
os_arch: str
os_version: str
shell_name: Literal["bash", "sh", "Windows PowerShell"]
shell_name: Literal["bash", "sh", "Windows PowerShell", "zsh"]
shell_path: KaosPath

@staticmethod
async def detect() -> Environment:
async def detect(shell_config: ShellConfig | None = None) -> Environment:
"""Detect environment with optional shell configuration."""
# Detect OS
match platform.system():
case "Darwin":
os_kind = "macOS"
Expand All @@ -30,29 +36,125 @@ async def detect() -> Environment:
os_arch = platform.machine()
os_version = platform.version()

# Determine shell based on OS
if os_kind == "Windows":
shell_name = "Windows PowerShell"
shell_path = KaosPath("powershell.exe")
shell_name, shell_path = await Environment._determine_windows_shell(shell_config)
else:
possible_paths = [
KaosPath("/bin/bash"),
KaosPath("/usr/bin/bash"),
KaosPath("/usr/local/bin/bash"),
]
fallback_path = KaosPath("/bin/sh")
for path in possible_paths:
if await path.is_file():
shell_name = "bash"
shell_path = path
break
else:
shell_name = "sh"
shell_path = fallback_path
shell_name, shell_path = await Environment._determine_unix_shell(shell_config)

return Environment(
os_kind=os_kind,
os_arch=os_arch,
os_version=os_version,
shell_name=shell_name,
shell_name=shell_name, # type: ignore[reportReturnType]
shell_path=shell_path,
)

@staticmethod
async def _determine_windows_shell(
shell_config: ShellConfig | None = None,
) -> tuple[str, KaosPath]:
"""Determine shell on Windows with priority: config > env var > auto-detect > fallback."""
config = shell_config or ShellConfig()

# Priority 1: Explicit path in config
if config.path:
path = KaosPath(config.path.replace("\\", "/"))
if await path.is_file():
shell_name = Environment._infer_shell_name(str(path))
return shell_name, path
logger.warning(
"Configured shell path not found: {path}, will use fallback shell detection",
path=config.path,
)

# Priority 2: SHELL environment variable (only when preferred="auto")
# This respects the user's terminal setup without requiring config changes
if config.preferred == "auto" and (env_shell := os.environ.get("SHELL")):
path = KaosPath(env_shell.replace("\\", "/"))
if await path.is_file():
shell_name = Environment._infer_shell_name(str(path))
return shell_name, path

# Priority 3: Auto-detect bash if preferred is "auto" or "bash"
if config.preferred in ("auto", "bash"):
bash_paths = [
# Git Bash - standard locations
KaosPath("C:/Program Files/Git/bin/bash.exe"),
KaosPath("C:/Program Files (x86)/Git/bin/bash.exe"),
KaosPath(os.path.expanduser("~/AppData/Local/Programs/Git/bin/bash.exe")),
# MSYS2
KaosPath("C:/msys64/usr/bin/bash.exe"),
KaosPath("C:/msys32/usr/bin/bash.exe"),
# Cygwin
KaosPath("C:/cygwin64/bin/bash.exe"),
KaosPath("C:/cygwin/bin/bash.exe"),
# WSL
KaosPath("C:/Windows/System32/bash.exe"),
]

for path in bash_paths:
if await path.is_file():
return "bash", path

# Priority 4: Fallback to PowerShell (backwards compatible default)
return "Windows PowerShell", KaosPath("powershell.exe")

@staticmethod
async def _determine_unix_shell(
shell_config: ShellConfig | None = None,
) -> tuple[str, KaosPath]:
"""Determine shell on Unix-like systems.

Note: config.preferred is intentionally not used on Unix. The standard
Unix convention is to respect SHELL env var and auto-detect from well-known
paths. Use config.path for explicit control.
"""
config = shell_config or ShellConfig()

# Priority 1: Explicit path in config
if config.path:
path = KaosPath(config.path)
if await path.is_file():
shell_name = Environment._infer_shell_name(str(path))
return shell_name, path
logger.warning(
"Configured shell path not found: {path}, will use fallback shell detection",
path=config.path,
)

# Priority 2: SHELL environment variable
if env_shell := os.environ.get("SHELL"):
path = KaosPath(env_shell)
if await path.is_file():
shell_name = Environment._infer_shell_name(str(path))
return shell_name, path

# Priority 3: Auto-detect common shells
possible_paths = [
KaosPath("/bin/bash"),
KaosPath("/usr/bin/bash"),
KaosPath("/usr/local/bin/bash"),
]
fallback_path = KaosPath("/bin/sh")

for path in possible_paths:
if await path.is_file():
return "bash", path

return "sh", fallback_path

@staticmethod
def _infer_shell_name(
path: str,
) -> Literal["bash", "sh", "Windows PowerShell", "zsh"]:
"""Infer shell name from executable path."""
path_lower = path.lower()
if "powershell" in path_lower or "pwsh" in path_lower:
return "Windows PowerShell"
elif "bash" in path_lower:
return "bash"
elif "zsh" in path_lower:
return "zsh"
else:
return "sh"
3 changes: 3 additions & 0 deletions tests/core/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from kimi_cli.config import (
Config,
Services,
ShellConfig,
get_default_config,
load_config_from_string,
)
Expand All @@ -21,6 +22,7 @@ def test_default_config():
models={},
providers={},
services=Services(),
shell=ShellConfig(),
)
)

Expand All @@ -41,6 +43,7 @@ def test_default_config_dump():
},
"services": {"moonshot_search": None, "moonshot_fetch": None},
"mcp": {"client": {"tool_call_timeout_ms": 60000}},
"shell": {"path": None, "preferred": "auto"},
}
)

Expand Down
Loading