Disable pagers in non-interactive code execution shells
The code execution tool runs commands inside TTY-backed shells (local PTY and remote SSH). Commands like `git diff`/`git log` detect the TTY and pipe output through a pager (more/less). These shells never receive interactive input, so the pager blocks forever and spins at 100% CPU per process — on a 16-core host 5 pager processes pegged 5 cores for 8+ hours (#1697). Disable pagers in both session types: - LocalInteractiveSession: inject PAGER=cat / GIT_PAGER=cat into the TTY env - SSHInteractiveSession: export the same in the initial shell command `cat` streams the output through instead of blocking, and also covers other pager-using tools (man, systemctl, journalctl). Adds regression tests. Fixes #1697
shisan committed
Jun 13, 2026 at 21:17 UTC
528c33b7ef0166e1ef31c98e9b3d290d37e732cc
3 files changed
+61
-2
plugins/_code_execution/helpers/shell_local.py
+22
-1
@@ -1,3 +1,4 @@
1
+import os
2
import platform
3
import select
4
import subprocess
@@ -8,6 +9,22 @@ from helpers import runtime
9
from plugins._code_execution.helpers import tty_session
10
from plugins._code_execution.helpers.shell_ssh import clean_string
11
12
+
13
+def disable_pagers_in_env(env: dict | None = None) -> dict:
14
+ """Return a copy of ``env`` with terminal pagers disabled.
15
+
16
+ Commands such as ``git diff``/``git log`` detect a TTY and pipe their output
17
+ through a pager (``more``/``less``). The non-interactive shells created by
18
+ the code execution tool never receive any user input, so the pager blocks
19
+ forever and spins at 100% CPU per process. Pointing the pager variables at
20
+ ``cat`` lets the output stream through instead. See issue #1697.
21
+ """
22
+ env = dict(env if env is not None else os.environ)
23
+ env["PAGER"] = "cat"
24
+ env["GIT_PAGER"] = "cat"
25
+ return env
26
+
27
+
28
class LocalInteractiveSession:
29
def __init__(self, cwd: str|None = None):
30
self.session: tty_session.TTYSession|None = None
@@ -15,7 +32,11 @@ class LocalInteractiveSession:
32
self.cwd = cwd
33
34
async def connect(self):
18
- self.session = tty_session.TTYSession(runtime.get_terminal_executable(), cwd=self.cwd)
35
+ self.session = tty_session.TTYSession(
36
+ runtime.get_terminal_executable(),
37
+ cwd=self.cwd,
38
+ env=disable_pagers_in_env(),
39
+ )
40
await self.session.start()
41
await self.session.read_full_until_idle(idle_timeout=1, total_timeout=1)
42
plugins/_code_execution/helpers/shell_ssh.py
+7
-1
@@ -8,6 +8,12 @@ from helpers.print_style import PrintStyle
8
# from helpers.strings import calculate_valid_match_lengths
9
10
11
+# Injected into every new SSH shell to keep it safe for non-interactive use.
12
+# Pagers (more/less) would otherwise block forever waiting for input that
13
+# never arrives and spin at 100% CPU; see issue #1697.
14
+PAGER_DISABLE_COMMAND = "export GIT_PAGER=cat; export PAGER=cat"
15
+
16
+
17
class SSHInteractiveSession:
18
19
# end_comment = "# @@==>> SSHInteractiveSession End-of-Command <<==@@"
@@ -63,7 +69,7 @@ class SSHInteractiveSession:
69
self.shell = self.client.invoke_shell(width=100, height=50)
70
71
# disable systemd/OSC prompt metadata and disable local echo
66
- initial_command = "unset PROMPT_COMMAND PS0; stty -echo"
72
+ initial_command = f"unset PROMPT_COMMAND PS0; stty -echo; {PAGER_DISABLE_COMMAND}"
73
if self.cwd:
74
initial_command = f"cd {self.cwd}; {initial_command}"
75
self.shell.send(f"{initial_command}\n".encode())
tests/test_code_execution_pager.py
new
+32
@@ -0,0 +1,32 @@
1
+"""Regression tests for issue #1697.
2
+
3
+Pagers (more/less) must be disabled in the non-interactive shells created by the
4
+code execution tool: without user input they block forever and spin at 100% CPU.
5
+"""
6
+
7
+from plugins._code_execution.helpers import shell_local, shell_ssh
8
+
9
+
10
+def test_local_env_disables_pagers_and_preserves_existing():
11
+ env = shell_local.disable_pagers_in_env({"PATH": "/usr/bin", "PAGER": "less"})
12
+ assert env["PAGER"] == "cat"
13
+ assert env["GIT_PAGER"] == "cat"
14
+ # pre-existing keys are preserved
15
+ assert env["PATH"] == "/usr/bin"
16
+
17
+
18
+def test_local_env_defaults_to_environ():
19
+ env = shell_local.disable_pagers_in_env()
20
+ assert env["PAGER"] == "cat"
21
+ assert env["GIT_PAGER"] == "cat"
22
+
23
+
24
+def test_local_env_does_not_mutate_input():
25
+ src = {"PATH": "/usr/bin"}
26
+ shell_local.disable_pagers_in_env(src)
27
+ assert src == {"PATH": "/usr/bin"}
28
+
29
+
30
+def test_ssh_command_disables_pagers():
31
+ assert "GIT_PAGER=cat" in shell_ssh.PAGER_DISABLE_COMMAND
32
+ assert "PAGER=cat" in shell_ssh.PAGER_DISABLE_COMMAND