Skip to content
Closed
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ Only write entries that are worth mentioning to users.
- Core: Fix connection recovery not triggering OAuth refresh when the retry returns 401 — after recreating the HTTP client on `APIConnectionError` or `APITimeoutError`, the retry now re-enters the full recovery path so a subsequent 401 correctly refreshes the OAuth token instead of bubbling to the user as an unrecoverable error
- Shell: Echo `/skill:*` and `/flow:*` inputs in the transcript so workflow commands stay visible after enter; operational slash commands like `/usage` and `/model` remain hidden
- Core: Raise default `max_steps_per_turn` from 500 to 1000 so long-running agents are less likely to hit the per-turn limit
- Core: Add `default_auto_approve_actions` config option — list of glob patterns for actions to auto-approve by default in every session; merged with any session-specific interactive approvals and supports wildcards such as `mcp:obsidian_*`
- Core: Add `auto_approve_workspace_dirs` config option — list of workspace directory names (relative to `work_dir`) where `WriteFile` and `StrReplaceFile` approvals are skipped automatically; useful for skills, plans, notes, or other directories the agent routinely modifies
- Core: New config files are now created from a commented TOML template with inline documentation and examples instead of a plain serialized dump
- Approval: Auto-approve actions now support glob pattern matching via `fnmatch` — exact match is checked first, then pattern expansion, so action names containing glob-special characters are handled safely


## 1.39.0 (2026-04-24)

Expand Down
8 changes: 8 additions & 0 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ The configuration file contains the following top-level configuration items:
| `theme` | `string` | Terminal color theme, either `"dark"` or `"light"` (defaults to `"dark"`) |
| `show_thinking_stream` | `boolean` | Whether to stream the raw reasoning text in the live area as a 6-line scrolling preview and commit the full reasoning markdown to history when the block ends (defaults to `true`; set to `false` to show only the compact `Thinking ...` indicator and a one-line trace summary) |
| `merge_all_available_skills` | `boolean` | Whether to merge skills from all brand directories (defaults to `true`); see [Skills configuration](../customization/skills.md) |
| `default_auto_approve_actions` | `array` | List of action name patterns to auto-approve by default in every session. Supports glob patterns (`*`, `?`). Examples: `["mcp:obsidian_*"]`. Defaults to `[]` |
| `auto_approve_workspace_dirs` | `array` | List of workspace directory names (relative to `work_dir`) for which file write/edit approvals should be skipped automatically. Example: `["skills", "plans"]`. Defaults to `[]` |
| `providers` | `table` | API provider configuration |
| `models` | `table` | Model configuration |
| `loop_control` | `table` | Agent loop control parameters |
Expand All @@ -51,6 +53,12 @@ theme = "dark"
show_thinking_stream = true
merge_all_available_skills = true

# Auto-approve all obsidian MCP tools by default
# default_auto_approve_actions = ["mcp:obsidian_*"]

# Skip approval for file edits inside these workspace directories
# auto_approve_workspace_dirs = ["skills", "plans"]

[providers.kimi-for-coding]
type = "kimi"
base_url = "https://api.kimi.com/coding/v1"
Expand Down
5 changes: 5 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ This page documents the changes in each Kimi Code CLI release.
- Core: Fix connection recovery not triggering OAuth refresh when the retry returns 401 — after recreating the HTTP client on `APIConnectionError` or `APITimeoutError`, the retry now re-enters the full recovery path so a subsequent 401 correctly refreshes the OAuth token instead of bubbling to the user as an unrecoverable error
- Shell: Echo `/skill:*` and `/flow:*` inputs in the transcript so workflow commands stay visible after enter; operational slash commands like `/usage` and `/model` remain hidden
- Core: Raise default `max_steps_per_turn` from 500 to 1000 so long-running agents are less likely to hit the per-turn limit
- Core: Add `default_auto_approve_actions` config option — list of glob patterns for actions to auto-approve by default in every session; merged with any session-specific interactive approvals and supports wildcards such as `mcp:obsidian_*`
- Core: Add `auto_approve_workspace_dirs` config option — list of workspace directory names (relative to `work_dir`) where `WriteFile` and `StrReplaceFile` approvals are skipped automatically; useful for skills, plans, notes, or other directories the agent routinely modifies
- Core: New config files are now created from a commented TOML template with inline documentation and examples instead of a plain serialized dump
- Approval: Auto-approve actions now support glob pattern matching via `fnmatch` — exact match is checked first, then pattern expansion, so action names containing glob-special characters are handled safely


## 1.39.0 (2026-04-24)

Expand Down
8 changes: 8 additions & 0 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ kimi --config '{"default_model": "kimi-for-coding", "providers": {...}, "models"
| `theme` | `string` | 终端配色主题,可选 `"dark"` 或 `"light"`(默认为 `"dark"`) |
| `show_thinking_stream` | `boolean` | 是否在 Live 区域以 6 行滚动预览方式实时展示模型的原始思考文本,并在 thinking 块结束时把完整思考内容(Markdown)写入历史记录(默认为 `true`;设为 `false` 则仅显示紧凑的 `Thinking ...` 指示器和一行 trace 总结) |
| `merge_all_available_skills` | `boolean` | 是否合并所有品牌目录中的 Skills(默认为 `true`);详见 [Skills 配置](../customization/skills.md) |
| `default_auto_approve_actions` | `array` | 每次会话默认自动审批的操作名称模式列表。支持 glob 通配符(`*`、`?`)。示例:`["mcp:obsidian_*"]`。默认为 `[]` |
| `auto_approve_workspace_dirs` | `array` | 文件写入/编辑操作自动跳过审批的工作区目录名称列表(相对于 `work_dir`)。示例:`["skills", "plans"]`。默认为 `[]` |
| `providers` | `table` | API 供应商配置 |
| `models` | `table` | 模型配置 |
| `loop_control` | `table` | Agent 循环控制参数 |
Expand All @@ -51,6 +53,12 @@ theme = "dark"
show_thinking_stream = true
merge_all_available_skills = true

# 默认自动审批所有 Obsidian MCP 工具
# default_auto_approve_actions = ["mcp:obsidian_*"]

# 在这些工作区目录内的文件编辑操作跳过审批
# auto_approve_workspace_dirs = ["skills", "plans"]

[providers.kimi-for-coding]
type = "kimi"
base_url = "https://api.kimi.com/coding/v1"
Expand Down
5 changes: 5 additions & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
- Core:修复连接恢复后重试返回 401 时未能触发 OAuth 刷新的问题——在 `APIConnectionError` 或 `APITimeoutError` 后重建 HTTP 客户端时,重试现在会重新进入完整恢复路径,使得后续的 401 能正确刷新 OAuth token,而不是作为不可恢复的错误直接抛给用户
- Shell:在 transcript 中回显 `/skill:*` 和 `/flow:*` 输入,工作流命令按下回车后不再消失;`/usage`、`/model` 等操作类斜杠命令仍然保持隐藏
- Core:将默认 `max_steps_per_turn` 从 500 提升到 1000,长任务更不容易撞到单轮步数上限
- Core:新增 `default_auto_approve_actions` 配置项——用于设置每次会话默认自动审批的操作名称模式列表,支持与会话级交互式自动审批合并,可使用 `mcp:obsidian_*` 等 glob 通配符
- Core:新增 `auto_approve_workspace_dirs` 配置项——用于设置自动跳过文件写入 / 编辑审批的工作区目录名称列表(相对于 `work_dir`),适用于 Skills、plans、notes 等 Agent 经常修改的目录
- Core:首次生成配置文件时,CLI 现在会写入带注释和示例的 TOML 模板,而不是纯序列化配置
- Approval:自动审批操作现在支持 glob 模式匹配——优先检查精确匹配,再进行模式扩展,因此包含 glob 特殊字符的操作名也能被安全处理


## 1.39.0 (2026-04-24)

Expand Down
126 changes: 123 additions & 3 deletions src/kimi_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,20 @@ class Config(BaseModel):
default=True,
description="Enable anonymous telemetry to help improve kimi-cli. Set to false to disable.",
)
default_auto_approve_actions: list[str] = Field(
default_factory=list,
description=(
"List of action name patterns to auto-approve by default in every session. "
"Supports glob patterns (*, ?). Examples: 'mcp:obsidian_*', 'edit file'."
),
)
auto_approve_workspace_dirs: list[str] = Field(
default_factory=list,
description=(
"List of workspace directory names (relative to work_dir) for which "
"file write/edit approvals should be skipped automatically."
),
)

@model_validator(mode="after")
def validate_model(self) -> Self:
Expand All @@ -279,6 +293,85 @@ def get_config_file() -> Path:
return get_share_dir() / "config.toml"


_DEFAULT_CONFIG_TEMPLATE = """\
# Kimi Code CLI configuration file
# Documentation: https://kimi-cli.github.io/configuration/config-files

# Default model to use for new sessions. Must match a key in the [models] table.
default_model = ""

# Default behavior flags
default_thinking = false
default_yolo = false
default_plan_mode = false

# External editor command (e.g. "vim", "code --wait"). Leave empty for auto-detect.
default_editor = ""

# Terminal color theme: "dark" or "light"
theme = "dark"

# Stream reasoning text in the live area? Set to false for a compact indicator only.
show_thinking_stream = true

# Merge skills from all brand directories (kimi, claude, codex, etc.)
merge_all_available_skills = true

# ------------------------------------------------------------------------------
# Auto-approval configuration
# ------------------------------------------------------------------------------
# Glob patterns for actions that should be auto-approved in EVERY session.
# These are merged with any session-specific approvals you make interactively.
# Examples:
# default_auto_approve_actions = ["mcp:obsidian_*"]
# default_auto_approve_actions = ["mcp:obsidian_*", "mcp:memory_*"]
default_auto_approve_actions = []

# Workspace directory names (relative to the current work_dir) where file
# write/edit approvals are skipped automatically. Useful for skills, plans,
# notes, or other directories the agent routinely modifies.
# Examples:
# auto_approve_workspace_dirs = ["skills", "plans"]
# auto_approve_workspace_dirs = ["docs", "notes"]
auto_approve_workspace_dirs = []

# Extra directories to discover skills from (absolute, ~-prefixed, or relative)
extra_skill_dirs = []

# Enable anonymous telemetry to help improve kimi-cli. Set to false to disable.
telemetry = true

# Suppress the YOLO mode hint injected into the system prompt.
skip_yolo_prompt_injection = false

[loop_control]
max_steps_per_turn = 500

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Config template hardcodes stale max_steps_per_turn = 500 instead of the current default 1000

The _DEFAULT_CONFIG_TEMPLATE at src/kimi_cli/config.py:348 sets max_steps_per_turn = 500, but the LoopControl model default was already bumped to 1000 at src/kimi_cli/config.py:79. The CHANGELOG even documents this change ("Raise default max_steps_per_turn from 500 to 1000"), and the docs at docs/en/configuration/config-files.md:73 show 1000. Every new user who gets a freshly generated config file will silently receive the old, lower limit. Additionally, the new test test_load_config_creates_template_with_comments (tests/core/test_config.py:99-102) asserts config.model_dump(...) == get_default_config().model_dump(), which will fail because the template parses to 500 while get_default_config() yields 1000.

Suggested change
max_steps_per_turn = 500
max_steps_per_turn = 1000
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep template max_steps_per_turn aligned with runtime default

The new default TOML template hard-codes max_steps_per_turn = 500, but LoopControl.max_steps_per_turn still defaults to 1000. On first run, load_config() now writes and validates this template, so fresh users silently get a lower step limit than the actual default model/config behavior and can hit max_steps_reached earlier. This is a regression from the intended 1000-step default and should be synchronized.

Useful? React with 👍 / 👎.

max_retries_per_step = 3
max_ralph_iterations = 0
reserved_context_size = 50000
compaction_trigger_ratio = 0.85

[background]
max_running_tasks = 4
read_max_bytes = 30000
notification_tail_lines = 20
notification_tail_chars = 3000
wait_poll_interval_ms = 500
worker_heartbeat_interval_ms = 5000
worker_stale_after_ms = 15000
kill_grace_period_ms = 2000
keep_alive_on_exit = false
agent_task_timeout_s = 900
print_wait_ceiling_s = 3600

[notifications]
claim_stale_after_ms = 15000

[mcp.client]
tool_call_timeout_ms = 60000
"""


def get_default_config() -> Config:
"""Get the default configuration."""
return Config(
Expand All @@ -289,6 +382,12 @@ def get_default_config() -> Config:
)


def _write_default_config_file(config_file: Path) -> None:
"""Write the default config file with comments and examples."""
config_file.parent.mkdir(parents=True, exist_ok=True)
config_file.write_text(_DEFAULT_CONFIG_TEMPLATE, encoding="utf-8")


def load_config(config_file: Path | None = None) -> Config:
"""
Load configuration from config file.
Expand All @@ -315,9 +414,30 @@ def load_config(config_file: Path | None = None) -> Config:
_migrate_json_config_to_toml()

if not config_file.exists():
config = get_default_config()
logger.debug("No config file found, creating default config: {config}", config=config)
save_config(config, config_file)
logger.debug("No config file found, creating default config at: {file}", file=config_file)
if config_file.suffix.lower() == ".json":
# Write a valid JSON default so subsequent loads via json.loads succeed.
default_config = get_default_config()
config_file.parent.mkdir(parents=True, exist_ok=True)
config_file.write_text(
json.dumps(
default_config.model_dump(mode="json", exclude_none=True),
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
config = default_config
else:
_write_default_config_file(config_file)
try:
data = tomlkit.loads(_DEFAULT_CONFIG_TEMPLATE)
config = Config.model_validate(data)
except (TOMLKitError, ValidationError) as e:
# This should never happen because the template is static and tested,
# but fall back to the plain default config if it does.
logger.warning("Default config template failed validation: {error}", error=e)
config = get_default_config()
config.is_from_default_location = is_default_config_file
config.source_file = config_file
return config
Expand Down
9 changes: 7 additions & 2 deletions src/kimi_cli/soul/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,21 @@ async def create(

# Merge CLI flag with persisted session state
effective_yolo = yolo or session.state.approval.yolo
default_actions = set(config.default_auto_approve_actions or [])
saved_actions = set(session.state.approval.auto_approve_actions)

def _on_approval_change() -> None:
session.state.approval.yolo = approval_state.yolo
session.state.approval.auto_approve_actions = set(approval_state.auto_approve_actions)
# Subtract config defaults before persisting so that removing a
# pattern from config actually disables it in existing sessions.
session.state.approval.auto_approve_actions = (
set(approval_state.auto_approve_actions) - default_actions
)
session.save_state()

approval_state = ApprovalState(
yolo=effective_yolo,
auto_approve_actions=saved_actions,
auto_approve_actions=default_actions | saved_actions,
on_change=_on_approval_change,
)
notifications = NotificationManager(
Expand Down
35 changes: 31 additions & 4 deletions src/kimi_cli/soul/approval.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import fnmatch
import re
import uuid
from collections.abc import Callable
from typing import Literal
Expand Down Expand Up @@ -52,6 +54,9 @@ def rejection_error(self) -> ToolRejectedError:
return ToolRejectedError()


_GLOB_SPECIAL_RE = re.compile(r"[*?\[\]]")


class ApprovalState:
def __init__(
self,
Expand All @@ -60,10 +65,32 @@ def __init__(
on_change: Callable[[], None] | None = None,
):
self.yolo = yolo
self.auto_approve_actions: set[str] = auto_approve_actions or set()
"""Set of action names that should automatically be approved."""
# Separate exact action names from glob patterns so that fnmatch is
# only applied to entries that actually contain glob-special characters.
self._auto_approve_exact: set[str] = set()
self._auto_approve_patterns: list[str] = []
for entry in (auto_approve_actions or set()):
if _GLOB_SPECIAL_RE.search(entry):
self._auto_approve_patterns.append(entry)
Comment on lines +73 to +74

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve exact auto-approve entries containing glob chars

This reclassification treats any persisted action containing *, ?, [ or ] as a pattern on reload, even if it was originally approved as an exact action via approve_for_session. Because persisted state stores only raw strings, exact approvals like plugin:tool[beta] can later stop matching themselves and/or match unrelated actions after restart, widening or changing approval scope across sessions.

Useful? React with 👍 / 👎.

else:
self._auto_approve_exact.add(entry)
self._on_change = on_change

def is_auto_approved(self, action: str) -> bool:
"""Return True if *action* matches an exact name or a glob pattern."""
if action in self._auto_approve_exact:
return True
return any(fnmatch.fnmatch(action, p) for p in self._auto_approve_patterns)

def add_auto_approve_action(self, action: str) -> None:
"""Add *action* as an exact auto-approve name (not a glob pattern)."""
self._auto_approve_exact.add(action)

@property
def auto_approve_actions(self) -> set[str]:
"""All auto-approve entries (exact names + glob patterns) for persistence."""
return self._auto_approve_exact | set(self._auto_approve_patterns)

def notify_change(self) -> None:
if self._on_change is not None:
self._on_change()
Expand Down Expand Up @@ -142,7 +169,7 @@ async def request(
)
return ApprovalResult(approved=True)

if action in self._state.auto_approve_actions:
if self._state.is_auto_approved(action):
from kimi_cli.telemetry import track

track(
Expand Down Expand Up @@ -195,7 +222,7 @@ async def request(
tool_name=tool_call.function.name,
approval_mode="manual",
)
self._state.auto_approve_actions.add(action)
self._state.add_auto_approve_action(action)
self._state.notify_change()
for pending in self._runtime.list_pending():
if pending.action == action:
Expand Down
29 changes: 26 additions & 3 deletions src/kimi_cli/tools/file/replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from kimi_cli.tools.utils import load_desc
from kimi_cli.utils.diff import build_diff_blocks
from kimi_cli.utils.logging import logger
from kimi_cli.utils.path import is_within_workspace
from kimi_cli.utils.path import is_within_directory, is_within_workspace

_BASE_DESCRIPTION = load_desc(Path(__file__).parent / "replace.md")

Expand Down Expand Up @@ -50,6 +50,7 @@ def __init__(self, runtime: Runtime, approval: Approval):
self._work_dir = runtime.builtin_args.KIMI_WORK_DIR
self._additional_dirs = runtime.additional_dirs
self._approval = approval
self._auto_approve_dirs = runtime.config.auto_approve_workspace_dirs or []
self._plan_mode_checker: Callable[[], bool] | None = None
self._plan_file_path_getter: Callable[[], Path | None] | None = None

Expand All @@ -60,6 +61,28 @@ def bind_plan_mode(
self._plan_mode_checker = checker
self._plan_file_path_getter = path_getter

def _is_auto_approved_dir(self, path: KaosPath) -> bool:
"""Check if path is within a configured auto-approve workspace directory."""
if not self._auto_approve_dirs:
return False
for dir_name in self._auto_approve_dirs:
# Reject absolute paths to prevent bypassing workspace boundaries;
# auto-approve dirs must be relative to work_dir.
if KaosPath(dir_name).is_absolute():
logger.warning("Ignoring absolute auto_approve_workspace_dir: {dir}", dir=dir_name)
continue
resolved = (self._work_dir / dir_name).canonical()
# Reject entries that escape work_dir via ".." traversal.
if not is_within_directory(resolved, self._work_dir):
logger.warning(
"Ignoring auto_approve_workspace_dir that escapes workspace: {dir}",
dir=dir_name,
)
continue
if is_within_directory(path, resolved):
return True
return False

async def _validate_path(self, path: KaosPath) -> ToolError | None:
"""Validate that the path is safe to edit."""
resolved_path = path.canonical()
Expand Down Expand Up @@ -155,8 +178,8 @@ async def __call__(self, params: Params) -> ToolReturnValue:
else FileActions.EDIT_OUTSIDE
)

# Plan file edits are auto-approved; all other edits need approval.
if not is_plan_file_edit:
# Plan file edits and auto-approved dirs are skipped; all other edits need approval.
if not is_plan_file_edit and not self._is_auto_approved_dir(p):
result = await self._approval.request(
self.name,
action,
Expand Down
Loading
Loading