diff --git a/news/ctrlz.rst b/news/ctrlz.rst
new file mode 100644
index 00000000..fa8c5d38
--- /dev/null
+++ b/news/ctrlz.rst
@@ -0,0 +1,27 @@
+**Added:**
+
+* New ``$THREAD_SUBPROCS`` environment variable allows you to
+  specify whether threadable subprocesses should actually be
+  run in a thread or not.  Default ``True``.
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Pressing ``Ctrl+Z`` no longer deadlocks the terminal,
+  allowing further input from the user, even for threaded
+  subprocesses.
+
+**Security:**
+
+* <news item>
diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py
index 586c0ad9..2794a226 100644
--- a/xonsh/built_ins.py
+++ b/xonsh/built_ins.py
@@ -738,7 +738,10 @@ class SubprocSpec:
         if not callable(alias):
             return
         self.is_proxy = True
-        thable = getattr(alias, "__xonsh_threadable__", True)
+        env = builtins.__xonsh__.env
+        thable = env.get("THREAD_SUBPROCS") and getattr(
+            alias, "__xonsh_threadable__", True
+        )
         cls = ProcProxyThread if thable else ProcProxy
         self.cls = cls
         self.threadable = thable
@@ -790,6 +793,7 @@ def _safe_pipe_properties(fd, use_tty=False):
 
 
 def _update_last_spec(last):
+    env = builtins.__xonsh__.env
     captured = last.captured
     last.last_in_pipeline = True
     if not captured:
@@ -799,9 +803,11 @@ def _update_last_spec(last):
         pass
     else:
         cmds_cache = builtins.__xonsh__.commands_cache
-        thable = cmds_cache.predict_threadable(
-            last.args
-        ) and cmds_cache.predict_threadable(last.cmd)
+        thable = (
+            env.get("THREAD_SUBPROCS")
+            and cmds_cache.predict_threadable(last.args)
+            and cmds_cache.predict_threadable(last.cmd)
+        )
         if captured and thable:
             last.cls = PopenThread
         elif not thable:
diff --git a/xonsh/environ.py b/xonsh/environ.py
index 500ce14e..6bc3e2d3 100644
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -571,6 +571,7 @@ def DEFAULT_ENSURERS():
         "SUGGEST_MAX_NUM": (is_int, int, str),
         "SUGGEST_THRESHOLD": (is_int, int, str),
         "SUPPRESS_BRANCH_TIMEOUT_MESSAGE": (is_bool, to_bool, bool_to_str),
+        "THREAD_SUBPROCS": (is_bool, to_bool, bool_to_str),
         "UPDATE_COMPLETIONS_ON_KEYPRESS": (is_bool, to_bool, bool_to_str),
         "UPDATE_OS_ENVIRON": (is_bool, to_bool, bool_to_str),
         "UPDATE_PROMPT_ON_KEYPRESS": (is_bool, to_bool, bool_to_str),
@@ -755,6 +756,7 @@ def DEFAULT_VALUES():
         "SUGGEST_COMMANDS": True,
         "SUGGEST_MAX_NUM": 5,
         "SUGGEST_THRESHOLD": 3,
+        "THREAD_SUBPROCS": True,
         "TITLE": DEFAULT_TITLE,
         "UPDATE_COMPLETIONS_ON_KEYPRESS": False,
         "UPDATE_OS_ENVIRON": False,
@@ -1159,6 +1161,26 @@ def DEFAULT_DOCS():
             "not always happen.",
             configurable=False,
         ),
+        "THREAD_SUBPROCS": VarDocs(
+            "Whether or not to try to run subrocess mode in a Python thread, "
+            "when applicable. There are various trade-offs, which normally "
+            "affects only interactive sessions.\n\nWhen True:\n\n"
+            "* Xonsh is able capture & store the stdin, stdout, and stderr \n"
+            "  of threadable subprocesses.\n"
+            "* However, stopping threaded subprocs with ^Z (i.e. ``SIGTSTP``)\n"
+            "  is disabled as it causes deadlocked terminals.\n"
+            "  ``SIGTSTP`` may still be issued and only the physical pressing\n"
+            "  of ``Ctrl+Z`` is ignored.\n"
+            "* Threadable commands are run with ``PopenThread`` and threadable \n"
+            "  aliases are run with ``ProcProxyThread``.\n\n"
+            "When False:\n\n"
+            "* Xonsh may not be able to capture stdin, stdout, and stderr streams \n"
+            "  unless explicitly asked to do so.\n"
+            "* Stopping the thread with ``Ctrl+Z`` yields to job control.\n"
+            "* Threadable commands are run with ``Popen`` and threadable \n"
+            "  alias are run with ``ProcProxy``.\n\n"
+            "The desired effect is often up to the command, user, or use case."
+        ),
         "TITLE": VarDocs(
             "The title text for the window in which xonsh is running. Formatted "
             "in the same manner as ``$PROMPT``, see 'Customizing the Prompt' "
diff --git a/xonsh/jobs.py b/xonsh/jobs.py
index ec984db1..20d5923b 100644
--- a/xonsh/jobs.py
+++ b/xonsh/jobs.py
@@ -202,7 +202,6 @@ else:
                     last_task=active_task, backgrounded=backgrounded
                 )
         if os.WIFSTOPPED(wcode):
-            print("^Z")
             active_task["status"] = "stopped"
             backgrounded = True
         elif os.WIFSIGNALED(wcode):
diff --git a/xonsh/proc.py b/xonsh/proc.py
index 9c778e9c..3c2899fe 100644
--- a/xonsh/proc.py
+++ b/xonsh/proc.py
@@ -554,6 +554,8 @@ class PopenThread(threading.Thread):
         self.timeout = env.get("XONSH_PROC_FREQUENCY")
         self.in_alt_mode = False
         self.stdin_mode = None
+        self._tc_cc_vsusp = b"\x1a"  # default is usually ^Z
+        self._disable_suspend_keybind()
         # stdout setup
         self.orig_stdout = stdout
         self.stdout_fd = 1 if stdout is None else stdout.fileno()
@@ -820,6 +822,33 @@ class PopenThread(threading.Thread):
             self.old_tstp_handler = None
         if frame is not None:
             self._disable_cbreak_stdin()
+        self._restore_suspend_keybind()
+
+    def _disable_suspend_keybind(self):
+        if ON_WINDOWS:
+            return
+        try:
+            mode = termios.tcgetattr(0)  # only makes sense for stdin
+        except termios.error:
+            return
+        self._tc_cc_vsusp = mode[CC][termios.VSUSP]
+        mode[CC][termios.VSUSP] = b"\x00"  # set ^Z (ie SIGSTOP) to undefined
+        termios.tcsetattr(0, termios.TCSANOW, mode)
+
+    def _restore_suspend_keybind(self):
+        if ON_WINDOWS:
+            return
+        try:
+            mode = termios.tcgetattr(0)  # only makes sense for stdin
+        except termios.error:
+            return
+        mode[CC][termios.VSUSP] = self._tc_cc_vsusp  # set ^Z (ie SIGSTOP) to original
+        try:
+            # this usually doesn't work in interactive mode,
+            # but we should try it anyway.
+            termios.tcsetattr(0, termios.TCSANOW, mode)
+        except termios.error:
+            pass
 
     #
     # SIGQUIT handler
@@ -1749,6 +1778,7 @@ def SIGNAL_MESSAGES():
         signal.SIGILL: "Illegal instructions",
         signal.SIGTERM: "Terminated",
         signal.SIGSEGV: "Segmentation fault",
+        signal.SIGTSTP: "Stopped",
     }
     if ON_POSIX:
         sm.update(
diff --git a/xonsh/ptk2/key_bindings.py b/xonsh/ptk2/key_bindings.py
index c3e78dc0..c68f9acb 100644
--- a/xonsh/ptk2/key_bindings.py
+++ b/xonsh/ptk2/key_bindings.py
@@ -350,3 +350,10 @@ def load_xonsh_bindings(key_bindings):
     @handle(Keys.ControlJ, filter=IsSearching())
     def accept_search(event):
         search.accept_search()
+
+    @handle(Keys.ControlZ)
+    def skip_control_z(event):
+        """Prevents the writing of ^Z to the prompt, if Ctrl+Z was pressed
+        during the previous command.
+        """
+        pass
diff --git a/xonsh/ptk2/shell.py b/xonsh/ptk2/shell.py
index d8d699f5..ec8b3f26 100644
--- a/xonsh/ptk2/shell.py
+++ b/xonsh/ptk2/shell.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 """The prompt_toolkit based xonsh shell."""
+import os
 import sys
 import builtins
 from types import MethodType
@@ -8,7 +9,7 @@ from xonsh.events import events
 from xonsh.base_shell import BaseShell
 from xonsh.shell import transform_command
 from xonsh.tools import print_exception, carriage_return
-from xonsh.platform import HAS_PYGMENTS, ON_WINDOWS
+from xonsh.platform import HAS_PYGMENTS, ON_WINDOWS, ON_POSIX
 from xonsh.style_tools import partial_color_tokenize, _TokenType, DEFAULT_STYLE_DICT
 from xonsh.lazyimps import pygments, pyghooks, winutils
 from xonsh.pygments_cache import get_all_styles
@@ -366,3 +367,9 @@ class PromptToolkit2Shell(BaseShell):
         #   if not ON_POSIX:
         #       return
         #   sys.stdout.write('\033[9999999C\n')
+        if not ON_POSIX:
+            return
+        stty, _ = builtins.__xonsh__.commands_cache.lazyget("stty", (None, None))
+        if stty is None:
+            return
+        os.system(stty + " sane")
diff --git a/xontrib/bashisms.py b/xontrib/bashisms.py
index 567602aa..9f620023 100644
--- a/xontrib/bashisms.py
+++ b/xontrib/bashisms.py
@@ -2,6 +2,7 @@
 import shlex
 import sys
 import re
+import builtins
 
 
 __all__ = ()
@@ -62,3 +63,4 @@ def alias(args, stdin=None):
 
 
 aliases["alias"] = alias
+builtins.__xonsh__.env["THREAD_SUBPROCS"] = False

ctrl-z problem on Linux
Launching a command (like emacs) followed by ctrl-z suspends
the process but doesn't return keyboard control to xonsh. So there
is no way to detach the process with bg and continue typing. The
documentation only mentions that ctrl-z doesn't work on Windows.

xonfig
+------------------+----------+
| xonsh | 0.3.2 |
| Python | 3.4.3 |
| PLY | 3.7 |
| have readline | True |
| prompt toolkit | None |
| shell type | readline |
| pygments | None |
| on posix | True |
| on linux | True |
| distro | Fedora |
| on darwin | False |
| on windows | False |
| is superuser | False |
| default encoding | utf-8 |
+------------------+----------+

diff --git a/tests/test_builtins.py b/tests/test_builtins.py
index 29950df6..36dcd2c5 100644
--- a/tests/test_builtins.py
+++ b/tests/test_builtins.py
@@ -6,6 +6,7 @@ import re
 import builtins
 import types
 from ast import AST, Module, Interactive, Expression
+from subprocess import Popen
 
 import pytest
 
@@ -25,8 +26,10 @@ from xonsh.built_ins import (
     in_macro_call,
     call_macro,
     enter_macro,
+    cmds_to_specs,
 )
 from xonsh.environ import Env
+from xonsh.proc import PopenThread, ProcProxy, ProcProxyThread
 
 from tools import skip_if_on_windows
 
@@ -388,3 +391,28 @@ def test_enter_macro():
     assert obj.macro_block == "wakka"
     assert obj.macro_globals
     assert obj.macro_locals
+
+
+@skip_if_on_windows
+def test_cmds_to_specs_thread_subproc(xonsh_builtins):
+    env = xonsh_builtins.__xonsh__.env
+    cmds = [["pwd"]]
+    # First check that threadable subprocs become threadable
+    env['THREAD_SUBPROCS'] = True
+    specs = cmds_to_specs(cmds, captured='hiddenobject')
+    assert specs[0].cls is PopenThread
+    # turn off threading and check we use Popen
+    env['THREAD_SUBPROCS'] = False
+    specs = cmds_to_specs(cmds, captured='hiddenobject')
+    assert specs[0].cls is Popen
+
+    # now check the threadbility of callable aliases
+    cmds = [[lambda: "Keras Selyrian"]]
+    # check that threadable alias become threadable
+    env['THREAD_SUBPROCS'] = True
+    specs = cmds_to_specs(cmds, captured='hiddenobject')
+    assert specs[0].cls is ProcProxyThread
+    # turn off threading and check we use ProcProxy
+    env['THREAD_SUBPROCS'] = False
+    specs = cmds_to_specs(cmds, captured='hiddenobject')
+    assert specs[0].cls is ProcProxy

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.7
requirements/docs.txt requirements/tests.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
Babel==2.14.0
black==23.3.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
cfgv==3.3.1
charset-normalizer==3.4.1
click==8.1.8
cloud-sptheme==1.10.1.post20200504175005
codecov==2.1.13
coverage==7.2.7
cryptography==44.0.2
cycler==0.11.0
distlib==0.3.9
doctr==1.9.0
docutils==0.19
exceptiongroup==1.2.2
filelock==3.12.2
flake8==3.9.2
fonttools==4.38.0
identify==2.5.24
idna==3.10
imagesize==1.4.1
importlib-metadata==6.7.0
iniconfig==2.0.0
Jinja2==3.1.6
kiwisolver==1.4.5
MarkupSafe==2.1.5
matplotlib==3.5.3
mccabe==0.6.1
mypy-extensions==1.0.0
nodeenv==1.9.1
numpy==1.21.6
numpydoc==1.5.0
packaging==24.0
pathspec==0.11.2
Pillow==9.5.0
platformdirs==4.0.0
pluggy==1.2.0
pre-commit==2.21.0
prompt_toolkit==3.0.48
psutil==7.0.0
py==1.11.0
pycodestyle==2.7.0
pycparser==2.21
pyflakes==2.3.1
Pygments==2.17.2
pyparsing==3.1.4
pytest==7.4.4
pytest-cov==4.1.0
pytest-flake8==1.1.0
pytest-timeout==2.3.1
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.1
pyzmq==26.2.1
requests==2.31.0
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==2.0.1
tornado==6.2
typed-ast==1.5.5
typing_extensions==4.7.1
urllib3==2.0.7
virtualenv==20.26.6
wcwidth==0.2.13
-e git+https://github.com/xonsh/xonsh.git@50155c66e3eb465c1bc328fad515220899acd361#egg=xonsh
zipp==3.15.0

name: xonsh
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.14.0
      - black==23.3.0
      - cffi==1.15.1
      - cfgv==3.3.1
      - charset-normalizer==3.4.1
      - click==8.1.8
      - cloud-sptheme==1.10.1.post20200504175005
      - codecov==2.1.13
      - coverage==7.2.7
      - cryptography==44.0.2
      - cycler==0.11.0
      - distlib==0.3.9
      - doctr==1.9.0
      - docutils==0.19
      - exceptiongroup==1.2.2
      - filelock==3.12.2
      - flake8==3.9.2
      - fonttools==4.38.0
      - identify==2.5.24
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - jinja2==3.1.6
      - kiwisolver==1.4.5
      - markupsafe==2.1.5
      - matplotlib==3.5.3
      - mccabe==0.6.1
      - mypy-extensions==1.0.0
      - nodeenv==1.9.1
      - numpy==1.21.6
      - numpydoc==1.5.0
      - packaging==24.0
      - pathspec==0.11.2
      - pillow==9.5.0
      - platformdirs==4.0.0
      - pluggy==1.2.0
      - pre-commit==2.21.0
      - prompt-toolkit==3.0.48
      - psutil==7.0.0
      - py==1.11.0
      - pycodestyle==2.7.0
      - pycparser==2.21
      - pyflakes==2.3.1
      - pygments==2.17.2
      - pyparsing==3.1.4
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - pytest-flake8==1.1.0
      - pytest-timeout==2.3.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.1
      - pyzmq==26.2.1
      - requests==2.31.0
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==2.0.1
      - tornado==6.2
      - typed-ast==1.5.5
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - virtualenv==20.26.6
      - wcwidth==0.2.13
      - zipp==3.15.0
prefix: /opt/conda/envs/xonsh

tests/test_builtins.py::test_cmds_to_specs_thread_subproc
tests/test_builtins.py::test_repath_backslash
tests/test_builtins.py::test_repath_HOME_PATH_itself tests/test_builtins.py::test_repath_HOME_PATH_contents tests/test_builtins.py::test_repath_HOME_PATH_var tests/test_builtins.py::test_repath_HOME_PATH_var_brace tests/test_builtins.py::test_helper_int tests/test_builtins.py::test_helper_helper tests/test_builtins.py::test_helper_env tests/test_builtins.py::test_superhelper_int tests/test_builtins.py::test_superhelper_helper tests/test_builtins.py::test_superhelper_env tests/test_builtins.py::test_ensure_list_of_strs[exp0-yo] tests/test_builtins.py::test_ensure_list_of_strs[exp1-inp1] tests/test_builtins.py::test_ensure_list_of_strs[exp2-42] tests/test_builtins.py::test_ensure_list_of_strs[exp3-inp3] tests/test_builtins.py::test_list_of_strs_or_callables[exp0-yo] tests/test_builtins.py::test_list_of_strs_or_callables[exp1-inp1] tests/test_builtins.py::test_list_of_strs_or_callables[exp2-42] tests/test_builtins.py::test_list_of_strs_or_callables[exp3-inp3] tests/test_builtins.py::test_list_of_strs_or_callables[exp4-<lambda>] tests/test_builtins.py::test_list_of_strs_or_callables[exp5-inp5] tests/test_builtins.py::test_list_of_list_of_strs_outer_product[inp0-exp0] tests/test_builtins.py::test_list_of_list_of_strs_outer_product[inp1-exp1] tests/test_builtins.py::test_list_of_list_of_strs_outer_product[inp2-exp2] tests/test_builtins.py::test_expand_path[~] tests/test_builtins.py::test_expand_path[~/] tests/test_builtins.py::test_expand_path[x=~/place] tests/test_builtins.py::test_expand_path[x=one:~/place] tests/test_builtins.py::test_expand_path[x=one:~/place:~/yo] tests/test_builtins.py::test_expand_path[x=~/one:~/place:~/yo] tests/test_builtins.py::test_convert_macro_arg_str[str0] tests/test_builtins.py::test_convert_macro_arg_str[s] tests/test_builtins.py::test_convert_macro_arg_str[S] tests/test_builtins.py::test_convert_macro_arg_str[str1] tests/test_builtins.py::test_convert_macro_arg_str[string] tests/test_builtins.py::test_convert_macro_arg_ast[AST] tests/test_builtins.py::test_convert_macro_arg_ast[a] tests/test_builtins.py::test_convert_macro_arg_ast[Ast] tests/test_builtins.py::test_convert_macro_arg_code[code0] tests/test_builtins.py::test_convert_macro_arg_code[compile0] tests/test_builtins.py::test_convert_macro_arg_code[c] tests/test_builtins.py::test_convert_macro_arg_code[code1] tests/test_builtins.py::test_convert_macro_arg_code[compile1] tests/test_builtins.py::test_convert_macro_arg_eval[type0] tests/test_builtins.py::test_convert_macro_arg_eval[t] tests/test_builtins.py::test_convert_macro_arg_eval[type1] tests/test_builtins.py::test_convert_macro_arg_exec[exec0] tests/test_builtins.py::test_convert_macro_arg_exec[x] tests/test_builtins.py::test_convert_macro_arg_exec[exec1] tests/test_builtins.py::test_in_macro_call tests/test_builtins.py::test_call_macro_str[x] tests/test_builtins.py::test_call_macro_str[42] tests/test_builtins.py::test_call_macro_str[x tests/test_builtins.py::test_call_macro_ast[x] tests/test_builtins.py::test_call_macro_ast[42] tests/test_builtins.py::test_call_macro_ast[x tests/test_builtins.py::test_call_macro_code[x] tests/test_builtins.py::test_call_macro_code[42] tests/test_builtins.py::test_call_macro_code[x tests/test_builtins.py::test_call_macro_eval[x] tests/test_builtins.py::test_call_macro_eval[42] tests/test_builtins.py::test_call_macro_eval[x tests/test_builtins.py::test_call_macro_exec[if tests/test_builtins.py::test_call_macro_raw_arg[x] tests/test_builtins.py::test_call_macro_raw_arg[42] tests/test_builtins.py::test_call_macro_raw_arg[x tests/test_builtins.py::test_call_macro_raw_kwarg[x] tests/test_builtins.py::test_call_macro_raw_kwarg[42] tests/test_builtins.py::test_call_macro_raw_kwarg[x tests/test_builtins.py::test_call_macro_raw_kwargs[x] tests/test_builtins.py::test_call_macro_raw_kwargs[42] tests/test_builtins.py::test_call_macro_raw_kwargs[x tests/test_builtins.py::test_call_macro_ast_eval_expr tests/test_builtins.py::test_call_macro_ast_single_expr tests/test_builtins.py::test_call_macro_ast_exec_expr tests/test_builtins.py::test_call_macro_ast_eval_statement tests/test_builtins.py::test_call_macro_ast_single_statement tests/test_builtins.py::test_call_macro_ast_exec_statement tests/test_builtins.py::test_enter_macro
xonsh/environ.py xonsh/ptk2/shell.py xonsh/jobs.py xonsh/built_ins.py xontrib/bashisms.py xonsh/proc.py news/ctrlz.rst xonsh/ptk2/key_bindings.py
xonsh/environ.py xonsh/ptk2/shell.py xonsh/jobs.py xonsh/built_ins.py xontrib/bashisms.py xonsh/proc.py news/ctrlz.rst xonsh/ptk2/key_bindings.py