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
20 changes: 14 additions & 6 deletions src/kimi_cli/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,19 +388,27 @@ def _emit_fatal_error(message: str) -> None:
case "okabe":
agent_file = OKABE_AGENT_FILE

# Track if -c/--command was explicitly used (vs --prompt/-p)
# to auto-enable print mode for command-style usage
command_mode = False
for arg in sys.argv[1:]:
if arg in ("-c", "--command"):
command_mode = True
break
Comment on lines +393 to +397
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.

🟡 Raw sys.argv scan for -c can match option values, causing false command_mode activation

The sys.argv scan at lines 394-397 iterates over all argv entries without understanding option-value pairing, so -c appearing as a value to another option (e.g., kimi --session -c) is incorrectly detected as command_mode = True.

Detailed Explanation

The code scans sys.argv[1:] linearly:

for arg in sys.argv[1:]:
    if arg in ("-c", "--command"):
        command_mode = True
        break

Consider kimi --session -c -p "hello". Typer parses --session as expecting a string value, so it consumes -c as the session ID (i.e., session_id = "-c"). The prompt parameter gets its value from -p "hello". However, the raw sys.argv scan finds -c at index 2 and sets command_mode = True, causing ui = "print" and implicitly enabling yolo at src/kimi_cli/cli/__init__.py:497. The user intended shell mode with session "-c" but instead gets print mode with auto-approval.

Impact: Unexpected UI mode and implicit yolo activation when -c happens to appear as a value to another option.

Open in Devin Review

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


if prompt is not None:
prompt = prompt.strip()
if not prompt:
raise typer.BadParameter("Prompt cannot be empty", param_hint="--prompt")

ui: UIMode = "shell"
if print_mode:
if print_mode or command_mode:
ui = "print"
Comment on lines +391 to 406
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.

🔴 command_mode bypasses UI conflict detection, silently overriding --acp/--wire and enabling yolo

When a user runs kimi -c "do something" --acp or kimi -c "do something" --wire, the command_mode flag silently overrides the explicitly requested UI mode and implicitly enables yolo (auto-approve all actions).

Root Cause

The command_mode variable is determined by raw sys.argv scanning at lines 393-397, but it is not included in the conflict_option_sets check at src/kimi_cli/cli/__init__.py:357-382. That check only examines print_mode:

conflict_option_sets = [
    {
        "--print": print_mode,   # only checks print_mode, not command_mode
        "--acp": acp_mode,
        "--wire": wire_mode,
    },
    ...
]

So when the user passes -c with --acp, the conflict check sees --print: False and --acp: True — only one active option — and passes. Then at line 405, command_mode takes priority:

if print_mode or command_mode:  # command_mode is True
    ui = "print"               # overrides --acp / --wire
elif acp_mode:
    ui = "acp"                 # never reached

Further, at line 497, ui == "print" implicitly enables yolo: yolo=yolo or (ui == "print"), meaning all tool actions are auto-approved without user consent.

Impact: The user's explicitly requested --acp or --wire mode is silently ignored, and dangerous auto-approval is enabled without the user's knowledge.

Prompt for agents
The command_mode flag must be integrated into the existing conflict detection system. In src/kimi_cli/cli/__init__.py, either:

1. Move the command_mode detection (lines 391-397) to BEFORE the conflict_option_sets block (line 357), and include command_mode in the conflict set alongside print_mode. For example, change the first conflict set to:
   {"--print": print_mode, "--command": command_mode, "--acp": acp_mode, "--wire": wire_mode}
   This ensures that -c/--command conflicts with --acp and --wire are detected and raise a clear error.

2. Alternatively, merge command_mode into print_mode before the conflict check:
   if command_mode:
       print_mode = True
   Then the existing conflict detection handles it naturally.
Open in Devin Review

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

elif acp_mode:
ui = "acp"
elif wire_mode:
ui = "wire"

if prompt is not None:
prompt = prompt.strip()
if not prompt:
raise typer.BadParameter("Prompt cannot be empty", param_hint="--prompt")

if input_format is not None and ui != "print":
raise typer.BadParameter(
"Input format is only supported for print UI",
Expand Down
34 changes: 21 additions & 13 deletions src/kimi_cli/utils/environment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import platform
import shutil
from dataclasses import dataclass
from typing import Literal

Expand Down Expand Up @@ -34,20 +35,27 @@ async def detect() -> Environment:
shell_name = "Windows PowerShell"
shell_path = KaosPath("powershell.exe")
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
# Use shutil.which first (works on NixOS and other non-FHS systems)
bash_in_path = shutil.which("bash")
if bash_in_path:
shell_name = "bash"
shell_path = KaosPath(bash_in_path)
Comment on lines +39 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize which() result to an absolute shell path

shutil.which("bash") may return a relative path when PATH contains relative entries (for example, bin/bash), and this value is stored directly as Environment.shell_path. Because the runtime later switches cwd to session.work_dir before executing shell commands, Shell can end up trying to exec a path that no longer resolves, causing Shell tool calls to fail when startup cwd and session cwd differ. Converting bash_in_path to an absolute path before storing it avoids this regression.

Useful? React with 👍 / 👎.

else:
shell_name = "sh"
shell_path = fallback_path
# Fallback to hardcoded paths for traditional systems
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

return Environment(
os_kind=os_kind,
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import os
import platform
import shutil
import tempfile
from collections.abc import Generator
from contextlib import contextmanager
Expand Down Expand Up @@ -143,12 +144,13 @@ def environment() -> Environment:
shell_path=KaosPath("powershell.exe"),
)
else:
bash_path = shutil.which("bash") or "/bin/bash"
return Environment(
os_kind="Unix",
os_arch="aarch64",
os_version="1.0",
shell_name="bash",
shell_path=KaosPath("/bin/bash"),
shell_path=KaosPath(bash_path),
)


Expand Down
16 changes: 15 additions & 1 deletion tests/core/test_default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# ruff: noqa

import platform
import re
import pytest
from inline_snapshot import snapshot
from kosong.tooling import Tool
Expand All @@ -12,6 +13,10 @@
from kimi_cli.soul.agent import Runtime


def _normalize_bash_path(text: str) -> str:
return re.sub(r"Execute a bash \(`[^`]+`\)", "Execute a bash (`/bin/bash`)", text)


@pytest.mark.skipif(platform.system() == "Windows", reason="Skipping test on Windows")
async def test_default_agent(runtime: Runtime):
agent = await load_agent(DEFAULT_AGENT_FILE, runtime, mcp_configs=[])
Expand Down Expand Up @@ -151,7 +156,16 @@ async def test_default_agent(runtime: Runtime):
- ALWAYS, keep it stupidly simple. Do not overcomplicate things.\
"""
)
assert agent.toolset.tools == snapshot(
# Normalize bash path in tool descriptions for cross-platform testing
normalized_tools = [
Tool(
name=tool.name,
description=_normalize_bash_path(tool.description),
parameters=tool.parameters,
)
for tool in agent.toolset.tools
]
assert normalized_tools == snapshot(
[
Tool(
name="Task",
Expand Down
5 changes: 4 additions & 1 deletion tests/tools/test_tool_descriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ def test_set_todo_list_description(set_todo_list_tool: SetTodoList):
@pytest.mark.skipif(platform.system() == "Windows", reason="Skipping test on Windows")
def test_shell_description(shell_tool: Shell):
"""Test the description of Shell tool."""
assert shell_tool.base.description == snapshot(
import re
desc = shell_tool.base.description
desc = re.sub(r"Execute a bash \(`[^`]+`\)", "Execute a bash (`/bin/bash`)", desc)
assert desc == snapshot(
"""\
Execute a bash (`/bin/bash`) command. Use this tool to explore the filesystem, edit files, run scripts, get system information, etc.

Expand Down
4 changes: 0 additions & 4 deletions tests/utils/test_pyinstaller_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ def test_pyinstaller_datas():
("src/kimi_cli/agents/default/sub.yaml", "kimi_cli/agents/default"),
("src/kimi_cli/agents/default/system.md", "kimi_cli/agents/default"),
("src/kimi_cli/agents/okabe/agent.yaml", "kimi_cli/agents/okabe"),
(
f"src/kimi_cli/deps/bin/{'rg.exe' if platform.system() == 'Windows' else 'rg'}",
"kimi_cli/deps/bin",
),
("src/kimi_cli/prompts/compact.md", "kimi_cli/prompts"),
("src/kimi_cli/prompts/init.md", "kimi_cli/prompts"),
(
Expand Down
5 changes: 5 additions & 0 deletions tests/utils/test_utils_environment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import platform
import shutil

import pytest
from kaos.path import KaosPath
Expand All @@ -10,6 +11,10 @@ async def test_environment_detection(monkeypatch):
monkeypatch.setattr(platform, "machine", lambda: "x86_64")
monkeypatch.setattr(platform, "version", lambda: "5.15.0-123-generic")

# Mock shutil.which to return None so fallback paths are used
import kimi_cli.utils.environment as env_module
monkeypatch.setattr(env_module.shutil, "which", lambda _: None)

async def _mock_is_file(self: KaosPath) -> bool:
return str(self) == "/usr/bin/bash"

Expand Down