Skip to content

feat(shell): inject shell mode output into context & persist cd#1587

Open
n-WN wants to merge 8 commits intoMoonshotAI:mainfrom
n-WN:feat/shell-mode-context-injection
Open

feat(shell): inject shell mode output into context & persist cd#1587
n-WN wants to merge 8 commits intoMoonshotAI:mainfrom
n-WN:feat/shell-mode-context-injection

Conversation

@n-WN
Copy link
Copy Markdown
Collaborator

@n-WN n-WN commented Mar 25, 2026

Summary

  • Shell mode (Ctrl+X) output now enters the conversation context as <system-reminder> user messages, so the agent knows what the user ran
  • cd is now persistent — resolved via real shell (~, -, CDPATH all work), os.chdir() + session.work_dir kept in sync
  • New shared capture.py module avoids duplicating logic between shell mode and future ! prefix

Architecture

capture.py — shared layer with four orthogonal functions

Function Responsibility
clean_output() \r\n normalize → C0 strip → \r overwrite resolve → ANSI strip via rich
execute_with_pty_capture() openpty + async subprocess + add_reader tee — TTY preserved
execute_with_pipe_capture() PIPE + tee (for future ! prefix, no TTY needed)
inject_to_context() truncate + <system-reminder> wrap, decoupled via append_message callback

PTY capture design

  • Subprocess stdout/stderr → PTY slave; stdin inherited (real terminal)
  • loop.add_reader(master_fd) tees output to real stdout + capture buffer
  • termios save/restore to handle commands that corrupt terminal state
  • KeyboardInterrupt → forward SIGINT → 3s timeout → kill
  • In-memory capture capped at _CAPTURE_HARD_LIMIT (100KB) to prevent cat /dev/urandom OOM
  • Terminal size propagated via TIOCGWINSZ/TIOCSWINSZ

cd persistence

  • cd target && pwd in real shell resolves all edge cases (~, -, $HOME, CDPATH, symlinks)
  • OLDPWD tracked in os.environ for cd - across invocations
  • session.work_dir updated so agent background tasks see the new directory

Test plan

  • uv run pytest tests — 1336 passed, 6 skipped
  • uv run pytest tests/e2e — 45 passed, 1 skipped, 1 xfailed
  • Manual: Ctrl+X → run ls, switch back to agent mode, ask "what did I just run" — agent should know
  • Manual: Ctrl+X → cd /tmppwd confirms /tmp → agent's Shell tool also runs in /tmp
  • Manual: cd - toggles back to previous directory
  • Manual: run a command that produces ANSI colors — context gets clean text

Open with Devin

…ersist cd

Shell mode (Ctrl+X) commands now capture output and inject it into the
conversation context as a <system-reminder> user message, so the agent
is aware of what the user ran.  `cd` is now persistent across commands
via os.chdir() with session.work_dir kept in sync.

Architecture:
- New `capture.py` shared module with three orthogonal concerns:
  - `clean_output()`: normalize \r\n, strip C0 controls, resolve \r
    overwrites, strip ANSI via rich
  - `execute_with_pty_capture()`: openpty + async subprocess +
    loop.add_reader tee — preserves TTY behaviour while capturing
  - `execute_with_pipe_capture()`: PIPE + tee for future `!` prefix
  - `inject_to_context()`: truncate + system-reminder wrap, decoupled
    from soul via append_message callback
- PTY capture includes: termios save/restore, SIGINT forwarding,
  in-memory capture hard limit (100KB), terminal size propagation
- `cd` resolved via real shell (supports ~, -, CDPATH), OLDPWD tracked
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bddb90f683

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +567 to +569
new_cwd = stdout.decode("utf-8", errors="replace").strip()
if not new_cwd or not os.path.isdir(new_cwd):
console.print(f"[red]cd: not a directory: {target}[/red]")
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 Handle cd - probe output as a single path

When the user runs cd -, POSIX shells print the destination directory to stdout; this probe also appends pwd, so stdout contains two lines. Here new_cwd is parsed with .strip() and then validated with os.path.isdir, which fails on the embedded newline, causing cd - to report “not a directory” even though the directory exists. Parse only the last non-empty stdout line (or avoid && pwd for cd -) before calling os.chdir.

Useful? React with 👍 / 👎.

Comment on lines +10 to +14
import fcntl
import os
import re
import sys
import termios
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 Guard POSIX-only capture imports on Windows

This new module imports fcntl and termios at module load time, but both are unavailable on Windows. Because src/kimi_cli/ui/shell/__init__.py now imports this module unconditionally, entering shell UI on Windows will raise ModuleNotFoundError before any command runs. Make these imports conditional (or lazy inside POSIX-only code paths) so shell UI remains importable on Windows.

Useful? React with 👍 / 👎.

devin-ai-integration[bot]

This comment was marked as resolved.

- Fix import sorting (I001)
- Replace try-except-pass with contextlib.suppress (SIM105)
- Replace asyncio.TimeoutError with builtin TimeoutError (UP041)
- Add logger usage in capture.py debug paths
- ruff format
@n-WN
Copy link
Copy Markdown
Collaborator Author

n-WN commented Mar 25, 2026

Simply put, fixed the issue where shell-mode did not enter the context @RealKai42

At the same time, allow the cd command in shell mode to switch directories (persist)

Windows adaptation will be completed tonight

Not yet make-changelog

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0abf650e5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +514 to +515
if split_cmd and split_cmd[0] == "cd":
await self._handle_cd(split_cmd)
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 Let non-builtin cd commands execute in the shell

This condition now routes every command whose first token is cd into _handle_cd, so valid shell commands like cd /tmp && ls or cd dir; pwd are rejected as too many arguments instead of executing. Before this change those commands ran normally in shell mode, so this is a functional regression for common workflows that chain cd with other operations.

Useful? React with 👍 / 👎.

Comment on lines +144 to +146
if captured_bytes < _CAPTURE_HARD_LIMIT:
captured.append(data)
captured_bytes += len(data)
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 Keep capture tail after reaching hard limit

The capture buffer stops appending once _CAPTURE_HARD_LIMIT is reached, which preserves the beginning of output, not the end. Since inject_to_context later keeps the last 50KB of what was captured, long commands end up injecting a mid-stream slice and can omit the final error/summary lines users and the agent typically need.

Useful? React with 👍 / 👎.

- Fix `cd ~` by expanding tilde on Python side before shlex.quote
- Fix `cd -` output parsing: take last non-empty line (shell prints
  destination before pwd)
- Guard fcntl/termios imports behind sys.platform != "win32"
- Fix capture to keep tail not head: evict old chunks when buffer
  exceeds 2x hard limit, so inject_to_context gets the most recent
  output
@n-WN n-WN requested review from RealKai42 March 25, 2026 23:39
@n-WN
Copy link
Copy Markdown
Collaborator Author

n-WN commented Mar 25, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5a6b51660

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +523 to +524
exit_code, raw_output = await execute_with_pty_capture(
command, env=get_clean_env(), cwd=os.getcwd()
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 Add non-POSIX fallback for shell command execution

_run_shell_command now unconditionally routes shell-mode commands through execute_with_pty_capture, but that helper depends on POSIX-only APIs (os.openpty, fcntl/termios, and loop.add_reader on file descriptors). In the shell UI path on Windows, this raises before command execution and the caller only prints a failure, so Ctrl+X shell mode cannot run normal commands there. Please gate PTY capture by platform and fall back to PIPE/create_subprocess_shell on non-POSIX runtimes.

Useful? React with 👍 / 👎.

Comment on lines 514 to 516
if split_cmd and split_cmd[0] == "cd":
await self._handle_cd(split_cmd)
return
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 Don't treat chained shell commands as invalid cd usage

The new cd interception triggers whenever the first token is cd, then _handle_cd rejects more than one argument; this makes valid shell commands like cd /tmp && ls or cd ..; pwd fail with cd: too many arguments instead of running. In shell mode this is a functional regression from the previous behavior, because any composite command beginning with cd is now blocked.

Useful? React with 👍 / 👎.


# Let the shell resolve -, $HOME, CDPATH, etc.
probe = await asyncio.create_subprocess_shell(
f"cd {shlex.quote(target)} && pwd",
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 Preserve env-var expansion for cd targets

Quoting the cd target in the probe command forces variable references to be literal, so inputs like cd $HOME attempt to enter a directory named $HOME instead of expanding the environment variable. This breaks common shell syntax in the new persistent-cd path and contradicts the intended behavior of resolving shell-style targets via the real shell.

Useful? React with 👍 / 👎.

Only intercept bare `cd` / `cd <path>` (at most 2 tokens). Compound
commands containing &&, ;, | etc. are passed through to the shell.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e4375ff6c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# Drain any remaining buffered output after process exits.
with contextlib.suppress(TimeoutError):
await asyncio.wait_for(eof_event.wait(), timeout=0.5)
except KeyboardInterrupt:
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 Catch cancellation when handling Ctrl+C in PTY runner

execute_with_pty_capture only handles KeyboardInterrupt, but this CLI is driven by asyncio.run, where SIGINT commonly cancels the main task and raises asyncio.CancelledError (a BaseException) instead. In that path the exception bypasses this handler and also bypasses _run_shell_command's except Exception, so pressing Ctrl+C during a shell-mode command can tear down the shell loop instead of returning to the prompt after stopping the child process.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +540 to +589
async def _handle_cd(self, args: list[str]) -> None:
"""Resolve ``cd`` via a real shell and persist the directory change.

Only called for bare ``cd`` / ``cd <path>`` (at most 2 tokens).
"""
target = args[1] if len(args) > 1 else "~"

# Expand ~ on Python side so shlex.quote won't suppress tilde expansion.
if target.startswith("~"):
target = os.path.expanduser(target)

# Provide OLDPWD so `cd -` works across invocations.
env = get_clean_env()
old_cwd = os.getcwd()
if "OLDPWD" not in env:
env["OLDPWD"] = old_cwd

# Let the shell resolve -, $HOME, CDPATH, etc.
probe = await asyncio.create_subprocess_shell(
f"cd {shlex.quote(target)} && pwd",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=env,
)
stdout, stderr = await probe.communicate()

if probe.returncode != 0:
err = stderr.decode("utf-8", errors="replace").strip()
console.print(f"[red]cd: {err or 'failed'}[/red]")
return

# `cd -` prints the destination before `pwd`; take the last non-empty line.
lines = [ln for ln in stdout.decode("utf-8", errors="replace").splitlines() if ln.strip()]
new_cwd = lines[-1].strip() if lines else ""
if not new_cwd or not os.path.isdir(new_cwd):
console.print(f"[red]cd: not a directory: {target}[/red]")
return

os.chdir(new_cwd)
# Set OLDPWD for the next `cd -` invocation.
os.environ["OLDPWD"] = old_cwd

# Keep the session's work_dir in sync so agent background tasks
# (which use session.work_dir as cwd) also see the new directory.
if isinstance(self.soul, KimiSoul):
from kaos.path import KaosPath

self.soul.runtime.session.work_dir = KaosPath.unsafe_from_local_path(
__import__("pathlib").Path(new_cwd)
)
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Mar 25, 2026

Choose a reason for hiding this comment

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

🔴 Unhandled exceptions in _handle_cd crash the interactive shell session

The _handle_cd method performs several fallible operations — os.getcwd() (line 555), asyncio.create_subprocess_shell (line 566), probe.communicate() (line 572), and os.chdir() (line 586) — but none are wrapped in try/except. The call site at src/kimi_cli/ui/shell/__init__.py:522 also lacks error handling. Any exception propagates through _run_shell_command into the main event loop (src/kimi_cli/ui/shell/__init__.py:453-454), which has only try/finally but no except, crashing the entire interactive session.

This contrasts with the general shell command path which wraps _execute_capture in try/except Exception at src/kimi_cli/ui/shell/__init__.py:529-535. A practical trigger: if the current working directory has been deleted (common during development), os.getcwd() at line 555 raises FileNotFoundError, killing the session instead of showing a friendly error.

Open in Devin Review

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

Comment on lines +161 to +175
try:
await proc.wait()
# Drain any remaining buffered output after process exits.
with contextlib.suppress(TimeoutError):
await asyncio.wait_for(eof_event.wait(), timeout=0.5)
except KeyboardInterrupt:
# Forward SIGINT to child if it is still running.
logger.debug("PTY capture: KeyboardInterrupt, forwarding SIGINT to child")
if proc.returncode is None:
proc.send_signal(2) # SIGINT
try:
await asyncio.wait_for(proc.wait(), timeout=3.0)
except (TimeoutError, ProcessLookupError):
logger.debug("PTY capture: child did not exit after SIGINT, killing")
proc.kill()
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Mar 25, 2026

Choose a reason for hiding this comment

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

🟡 asyncio.CancelledError silently swallowed in execute_with_pty_capture

The PR added asyncio.CancelledError to the exception handler at src/kimi_cli/ui/shell/capture.py:166, but the exception is caught and not re-raised after cleanup. This means if the enclosing asyncio task is cancelled during proc.wait(), the cancellation is silently suppressed — the function returns normally with partial output instead of propagating the cancellation. While KeyboardInterrupt (also caught here) is conventionally handled without re-raising in interactive apps, CancelledError should be re-raised after cleanup to honor the task cancellation contract. Without re-raising, the caller (_run_shell_command) will proceed to inject context into the conversation and return normally, defeating the cancellation.

Open in Devin Review

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6537e0c17b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +274 to +278
body = f"$ {command}\n{output}"

message = Message(
role="user",
content=[system_reminder(f"{header}\n\n{body}")],
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 Escape shell text before system-reminder injection

inject_to_context embeds raw command/output directly inside a <system-reminder> wrapper, so shell output containing </system-reminder> (or nested <system-reminder>...) can break out and introduce new authoritative directives. This is exploitable whenever a user runs commands over untrusted content (for example cat on a repo file), and it is high impact because the default agent prompt explicitly treats any <system-reminder> tags as system-level instructions (src/kimi_cli/agents/default/system.md).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Comment on lines +562 to +563
needs_shell_expansion = target.startswith("~") or target.startswith("$") or target == "-"
quoted_target = target if needs_shell_expansion else shlex.quote(target)
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.

🔴 Unquoted tilde/dollar targets in _handle_cd break paths containing spaces

The PR removed the Python-side os.path.expanduser() call for ~ targets and replaced it with passing the target unquoted to the shell when it starts with ~, $, or equals -. This regresses cd for paths containing spaces. For example, cd "~/My Documents" (shlex.split → ['cd', '~/My Documents']) previously worked because the old code expanded ~ with os.path.expanduser (→ /home/user/My Documents) then safely quoted it with shlex.quote. Now the unquoted target produces the shell command cd ~/My Documents && pwd, where the shell sees ~/My and Documents as separate words, causing cd to fail.

Reproduction trace

Old code path: target="~/My Documents"os.path.expanduser(...)"/home/user/My Documents"shlex.quote(...)"'/home/user/My Documents'" → shell: cd '/home/user/My Documents' && pwd → ✅

New code path: target="~/My Documents"needs_shell_expansion=Truequoted_target="~/My Documents" (unquoted) → shell: cd ~/My Documents && pwd → ❌ (word-split by shell)

Prompt for agents
In src/kimi_cli/ui/shell/__init__.py, lines 559-567 in _handle_cd: the current approach of passing tilde and dollar targets unquoted to the shell breaks when the target contains spaces (e.g. cd ~/My Documents). The fix should properly handle shell expansion while also quoting for spaces. One approach is to use shlex.quote on the target but selectively handle tilde expansion: for ~ targets, expand on the Python side with os.path.expanduser first (as the old code did), then shlex.quote the result. For $ targets, you could expand with os.path.expandvars first, then shlex.quote. For - targets, shlex.quote('-') works fine. This ensures both shell-like expansion AND proper handling of spaces/special characters.
Open in Devin Review

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57c01db6c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +520 to +521
if split_cmd and split_cmd[0] == "cd" and len(split_cmd) <= 2:
await self._handle_cd(split_cmd)
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 Skip cd interception when shell operators are attached

The len(split_cmd) <= 2 gate still captures commands like cd /tmp&&pwd or cd ~/x;ls because shlex.split keeps &&/; inside the second token when no spaces are present. Those are valid shell command chains, but this branch routes them into _handle_cd instead of executing normally: for quoted targets it fails as cd '/tmp&&pwd', and for ~/$ targets it can execute extra probe-side commands with output swallowed. This is a functional regression for common no-space shell syntax.

Useful? React with 👍 / 👎.

Comment on lines 530 to 533
execute_with_pty_capture if sys.platform != "win32" else execute_with_pipe_capture
)
exit_code, raw_output = await execute(command, env=get_clean_env(), cwd=os.getcwd())
except Exception as e:
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 shell loop on Ctrl+C in Windows fallback path

On win32, this function now calls execute_with_pipe_capture, but the surrounding handler only catches Exception. Interrupts during shell commands are raised as KeyboardInterrupt/asyncio.CancelledError (BaseException), so they bypass this block and can tear down the shell loop instead of returning to the prompt after stopping the subprocess. The previous implementation installed explicit SIGINT handling, so this is a regression for interactive interruption on the Windows execution path.

Useful? React with 👍 / 👎.

- Windows: fall back to pipe capture when PTY is unavailable
- PTY: catch asyncio.CancelledError alongside KeyboardInterrupt
  to prevent Ctrl+C from tearing down the shell loop
- Security: neutralise <system-reminder> tags in captured output
  to prevent injection via untrusted shell output
- cd: preserve shell expansion for ~, $VAR, and - targets instead
  of quoting them with shlex.quote
@n-WN n-WN force-pushed the feat/shell-mode-context-injection branch from 57c01db to 96a434c Compare March 27, 2026 17:12
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96a434c44a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +562 to +563
needs_shell_expansion = target.startswith("~") or target.startswith("$") or target == "-"
quoted_target = target if needs_shell_expansion else shlex.quote(target)
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 quoting when resolving expandable cd targets

Because shlex.split() removes the user's original quotes, this branch emits target unquoted whenever it starts with ~ or $, then splices it into cd {quoted_target} && pwd. That breaks valid inputs like cd ~/'tmp space' or cd "$HOME/tmp space" (the probe sees multiple args and fails), and it can also reinterpret quoted literals containing shell metacharacters. The persistent-cd path should not drop quoting semantics for expansion cases.

Useful? React with 👍 / 👎.

@n-WN
Copy link
Copy Markdown
Collaborator Author

n-WN commented Mar 27, 2026

@codex review

2 similar comments
@n-WN
Copy link
Copy Markdown
Collaborator Author

n-WN commented Mar 27, 2026

@codex review

@n-WN
Copy link
Copy Markdown
Collaborator Author

n-WN commented Mar 27, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96a434c44a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


# Let the shell resolve ~, -, $HOME, CDPATH, etc.
probe = await asyncio.create_subprocess_shell(
f"cd {quoted_target} && pwd",
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 Replace POSIX-only pwd in cd resolver

The cd probe command is hardcoded as cd {quoted_target} && pwd, which assumes a POSIX shell, but _handle_cd is called on all platforms. In Windows shell mode, asyncio.create_subprocess_shell runs via cmd.exe, where pwd is not a builtin, so the probe fails and every cd command is treated as an error; as a result, os.chdir() and session.work_dir are never updated for Windows users.

Useful? React with 👍 / 👎.

# expansion: ~, -, and $VAR references. For those we let the real
# shell handle expansion directly.
needs_shell_expansion = target.startswith("~") or target.startswith("$") or target == "-"
quoted_target = target if needs_shell_expansion else shlex.quote(target)
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 Preserve quoting for expandable cd targets

For targets starting with ~ or $, the code intentionally bypasses shlex.quote and injects the raw token into the probe command, which loses the user's quoting context. Valid commands like cd ~/"Application Support" or cd "$HOME/My Folder" become unquoted (cd ~/Application Support && pwd), get word-split by the shell, and fail even though the original command is valid.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 165b13b6f5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +562 to +563
needs_shell_expansion = target.startswith("~") or target.startswith("$") or target == "-"
quoted_target = target if needs_shell_expansion else shlex.quote(target)
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 Preserve env expansion beyond first character in cd target

The cd resolver only treats targets as expandable when target.startswith("$"), so commands like cd src/$MODULE or cd ./$PROJECT are incorrectly quoted and passed to the shell as literals (e.g. cd 'src/$MODULE'), which prevents variable expansion and makes valid shell paths fail. This is a functional regression in the new persistent-cd flow for users who compose paths with env vars outside the first character.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

Comment on lines +205 to +232
proc = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
env=env,
cwd=cwd,
)
assert proc.stdout is not None
chunks: list[bytes] = []
captured_bytes = 0
while True:
chunk = await proc.stdout.read(4096)
if not chunk:
break
try:
sys.stdout.buffer.write(chunk)
sys.stdout.buffer.flush()
except (OSError, AttributeError):
pass
chunks.append(chunk)
captured_bytes += len(chunk)
if captured_bytes > _CAPTURE_HARD_LIMIT * 2:
while chunks and captured_bytes > _CAPTURE_HARD_LIMIT:
evicted = chunks.pop(0)
captured_bytes -= len(evicted)
await proc.wait()
raw = b"".join(chunks).decode("utf-8", errors="replace")
return proc.returncode, raw
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Mar 27, 2026

Choose a reason for hiding this comment

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

🔴 Missing KeyboardInterrupt handling in execute_with_pipe_capture causes shell crash on Windows

On Windows, execute_with_pipe_capture is selected as _execute_capture (src/kimi_cli/ui/shell/__init__.py:32). Unlike execute_with_pty_capture which explicitly catches (KeyboardInterrupt, asyncio.CancelledError) at src/kimi_cli/ui/shell/capture.py:166, the pipe capture variant has no such handling. When Ctrl+C is pressed during a shell command on Windows, KeyboardInterrupt (a BaseException) propagates out of execute_with_pipe_capture, through _run_shell_command (whose except Exception at src/kimi_cli/ui/shell/__init__.py:533 does not catch BaseException subclasses), and up through the shell's run() loop which also lacks a KeyboardInterrupt catch — crashing the interactive session. The old code installed a custom SIGINT handler via install_sigint_handler that gracefully terminated the child process and returned control to the shell prompt. Additionally, the child process is not explicitly terminated and stdout pipe resources are not cleaned up.

Open in Devin Review

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 000e82bc93

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

parts.append(f"(Output truncated to last {SHELL_OUTPUT_MAX_BYTES} bytes)")

header = " ".join(parts)
body = f"$ {command}\n{output}"
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 Escape command text before system-reminder injection

inject_to_context now escapes <system-reminder> tags in output, but it still interpolates raw command into the same wrapped reminder body. A shell command containing </system-reminder> (for example via copied untrusted command text) will prematurely close the wrapper and can inject new authoritative reminders into model context, because this content is later embedded inside system_reminder(...) without sanitizing the command string.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

parts.append(f"(Output truncated to last {SHELL_OUTPUT_MAX_BYTES} bytes)")

header = " ".join(parts)
body = f"$ {command}\n{output}"
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 string not sanitized for system-reminder tag injection in inject_to_context

The output is explicitly sanitized to neutralize <system-reminder> / </system-reminder> tags (lines 258-259), but the command string is embedded in the body at line 278 (f"$ {command}\n{output}") without the same sanitization. Since the body is wrapped via system_reminder() (src/kimi_cli/soul/message.py:24) which adds <system-reminder>…</system-reminder> tags, a command containing </system-reminder> (e.g. from a copy-pasted script or test command) would prematurely close the outer tag, corrupting the structured message. This is inconsistent with the comment at line 257 stating the intent to "prevent injection".

Suggested change
body = f"$ {command}\n{output}"
command_safe = command.replace("<system-reminder>", "&lt;system-reminder&gt;").replace("</system-reminder>", "&lt;/system-reminder&gt;")
body = f"$ {command_safe}\n{output}"
Open in Devin Review

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant