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