When does SSH TTY allocation change a command?
SSH TTY allocation changes prompts, streams, signals, and terminal detection. Learn when to use -T, -t, or -tt without breaking automation.

Adding -t to an SSH command does much more than make a prompt appear. It replaces three separate remote streams with a terminal, installs terminal line discipline, gives the session a controlling terminal, and invites programs to behave as if a person is watching. That can fix an interactive sudo run while quietly breaking a parser, a pipeline, or cancellation.
For unattended work, no PTY should be the default. Request one only when the remote program actually requires terminal semantics, and treat that choice as part of the command contract. I have seen too many deployment scripts acquire -tt during an emergency and keep it for years, long after anyone remembered which failure it concealed.
A PTY changes the process environment
SSH TTY allocation changes what file descriptors the remote process receives, not merely how its output looks. With no PTY, OpenSSH connects standard input, standard output, and standard error through pipes or socket pairs. With a PTY, OpenSSH makes the PTY the controlling terminal and duplicates the same terminal descriptor onto all three standard streams.
That implementation detail has visible consequences. A program can call isatty() and select an interactive code path. The terminal driver can process special input characters, echo input, convert line endings, and track a foreground process group. Standard error no longer has an independent remote channel because both output descriptors point at the same terminal.
RFC 4254 keeps these ideas separate at protocol level. A session may request pty-req, then request a shell or an exec command. The PTY request includes a terminal type, dimensions, and encoded terminal modes. Nothing in that request changes an exec request into a login shell.
OpenSSH exposes the choice with three useful forms:
ssh -T host commanddisables PTY allocation explicitly.ssh -t host commandrequests a PTY when the local client has a terminal.ssh -tt host commandforces the request even when local input is a pipe or another nonterminal source.
The double -t is not a stronger remote terminal. It overrides the local safety check. That distinction matters in a build runner, an agent process, or printf ... | ssh, where a single -t can print "Pseudo-terminal will not be allocated because stdin is not a terminal" and continue without the semantics the author expected.
No PTY also preserves a transparent byte path. The OpenSSH ssh(1) manual calls that mode suitable for reliable binary transfer. A PTY is a character device with processing rules, so it is the wrong transport for an archive, a database dump, or machine-readable output whose bytes must remain exact.
The same choice can hide in configuration. OpenSSH's RequestTTY client setting accepts no, yes, force, or auto, corresponding to the command line behaviors people usually reach through -T, -t, and the default. Check the effective client configuration with ssh -G host.example when a command allocates a terminal you did not request on its command line. Host blocks and included files can otherwise turn two identical looking commands into different sessions.
The server gets a vote as well. PermitTTY can deny allocation, and an authorized key entry can carry a no-pty restriction. A forced command runs on a PTY only when the client requests one and the server permits it. Treat refusal as a real interface mismatch, not as a reason to keep retrying with more -t flags.
Measure both modes before changing them
A small probe reveals more than another round of SSH option guessing. Run it against the same account, host configuration, and command path that production uses:
probe='
for fd in 0 1 2; do
if test -t "$fd"; then kind=tty; else kind=not-tty; fi
printf "fd%s=%s\n" "$fd" "$kind"
done
printf "term=%s\n" "${TERM-unset}"
printf "stdout-marker\n"
printf "stderr-marker\n" >&2
exit 23
'
ssh -T host.example "$probe" >no-pty.out 2>no-pty.err
printf 'no-pty ssh status=%s\n' "$?"
ssh -tt host.example "$probe" >pty.out 2>pty.err
printf 'pty ssh status=%s\n' "$?"
The nonterminal run should put the three fdN=not-tty lines, term=unset, and stdout-marker in no-pty.out. It should put only stderr-marker in no-pty.err. The exact TERM result can vary because clients and servers may accept environment settings differently, so record what your actual route supplies rather than depending on an example.
The PTY run should report all three descriptors as tty. Both markers normally arrive in pty.out, often with carriage return plus line feed line endings. pty.err contains local SSH diagnostics, if any, but it cannot recover the remote program's original standard error as a separate stream. Both SSH commands should return 23 because PTY allocation alone does not discard the remote exit status.
Extend the probe with the actual executable before changing a job. Add command -V tool, pwd, a filtered environment print, and the program's nonsecret diagnostic flags. If output changes, identify which observation caused the branch: terminal detection, TERM, terminal width, merged streams, a startup file, or a prompt. "Works with -t" is only a symptom report.
Run the probe through the same launcher too. A terminal command pasted into a shell has a local TTY; an agent or continuous integration worker often does not. That is why an experiment with -t at a keyboard can disagree with the automated run until you test -tt, and why forcing a PTY can expose a job to input it never expected.
Test input separately from output. ssh -n redirects the client's standard input from /dev/null, and the StdinNull setting does the same through configuration. That is useful when a remote command must never consume the caller's input, but it does not disable PTY allocation by itself. A forced PTY with null input still changes terminal detection, stream merging, and hangup behavior.
Be careful when SSH runs inside a loop. Without -n, the first remote command can read lines intended for the loop, whether or not the remote process was meant to consume them. With a PTY, terminal echo can also send those bytes back into the transcript. Make the ownership of standard input explicit before interpreting any other difference between runs.
Sudo prompts need an explicit contract
sudo needs a way to authenticate, and a PTY gives it a terminal from which it can read a password. The sudo manual says that it normally reads the password from the user's terminal. Without one, a password requirement fails unless an askpass helper exists or the caller selects another input method.
That explains the familiar fix:
ssh -t host.example 'sudo systemctl restart example.service'
It is acceptable for a human who intends to type a password and watch the command. It is a poor repair for unattended execution. The job can wait forever for input, a prompt can contaminate expected output, and somebody may eventually pipe a password into the same channel as command data.
Use noninteractive sudo for automation:
ssh -T host.example \
'sudo -n /usr/bin/systemctl restart example.service'
The -n option tells sudo not to prompt. If cached credentials or sudoers policy do not authorize the action without interaction, sudo exits with an error. That failure is useful: the caller receives a definite status instead of a hidden prompt. Pair it with a narrow sudoers rule for an exact command and arguments when privileged automation is truly required. A blanket passwordless shell trades an availability problem for a much larger authority problem.
Do not depend on a cached sudo timestamp for an unattended job. The cache belongs to an authentication context whose terminal and session details vary by policy. A job that succeeds because an administrator used sudo a minute earlier and fails after a quiet weekend does not have an authorization design; it has timing luck.
sudo -S reads a password from standard input. It can make a no-PTY command run, but it also mixes a secret with the data path and puts password handling into the caller. Avoid it for agents and build jobs. An askpass helper can suit a graphical human workflow, but it still represents interactive authentication and must not be invented inside a supposedly unattended job.
Two PTYs are often confused here. SSH may allocate a remote PTY before starting sudo. Separately, modern sudo can run the authorized command in its own PTY for I/O logging and isolation; sudo 1.9.14 enables its use_pty setting by default. That inner PTY does not provide the outer password prompt when the SSH session has no terminal. It begins after sudo has enough context to run the command.
Some sudoers configurations also require a terminal as policy. Older enterprise configurations commonly used requiretty, and a current system can still carry it. Confirm the effective sudoers policy instead of assuming every "terminal is required" message means a password prompt.
A PTY does not choose shell startup files
An SSH command with a PTY is still a command run through the account's configured shell with -c. The OpenSSH sshd(8) manual states that a requested command is executed that way, and the server source passes the command to the user's shell. PTY allocation changes the descriptors around that shell; it does not add -i, -l, or a login option.
This matters when PATH, a language manager, or an alias exists at an interactive prompt but not in automation. Adding -t may appear to fix the path because a startup file contains terminal tests or because the called tool detects a terminal. It may also do nothing. Depending on it leaves the job coupled to dotfiles that developers edit for their own terminals.
Bash has a special case that makes the folklore harder to untangle. Its manual says an interactive login shell reads profile files, an interactive nonlogin shell reads .bashrc, and a noninteractive shell uses BASH_ENV. Bash also tries to recognize execution by a remote shell daemon and may read .bashrc even when noninteractive, unless invoked as sh. That exception is Bash behavior, not a promise made by SSH and not portable to every account shell.
Select the shell mode directly when you need one:
# A predictable noninteractive Bash command with explicit inputs
ssh -T host.example \
'PATH=/usr/local/bin:/usr/bin:/bin /bin/bash -c '\''command -v deploy && deploy'\'''
# A login environment, requested because the command truly depends on it
ssh -T host.example \
'/bin/bash -lc '\''command -v deploy && deploy'\'''
The second form deliberately reads login startup files and inherits their risks: output printed by a profile can corrupt a protocol, and a profile can change behavior without a deployment review. For production jobs, an absolute executable path and a small explicit environment are usually better.
Aliases are another trap. Noninteractive Bash does not expand aliases unless expand_aliases is enabled, and a shell function exists only if a startup mechanism defines or imports it. If ssh host deploy works only after you added -t, verify that deploy is a real executable. A terminal should not decide whether the remote account can locate the program.
Remember that SSH sends a command string, not an argument vector for the final executable. The remote account shell parses that string once, after the local shell has already processed its own quoting. PTY allocation does not change either parsing pass. When values can contain spaces or shell metacharacters, send a script with fixed inputs, use a remote helper with a defined data format, or quote each layer deliberately. A terminal cannot repair an argument that the local shell expanded too early.
Ctrl-C follows terminal rules only with a PTY
With a PTY, Ctrl-C is normally an input byte interpreted by the remote terminal driver. POSIX calls it the INTR character. When the ISIG terminal flag is set, the driver discards that byte and sends SIGINT to every process in the terminal's foreground process group. This is why a remote foreground pipeline can stop as one job.
The local SSH client puts the local terminal into a suitable mode and sends keystrokes across the channel. The remote PTY owns canonical input, echo, special characters, and window size. Programs such as full-screen editors may change those modes, and an abnormal disconnect can leave display state looking wrong until the local terminal is reset.
Without a PTY, there is no remote terminal driver to interpret Ctrl-C and no terminal foreground group. RFC 4254 defines a separate SSH signal channel request, but it also allows systems that do not implement signals to ignore it. A client, server, wrapper, and called program can therefore produce cancellation behavior that differs from an interactive terminal.
Ctrl-Z and job control expose the difference even faster. In a terminal, the SUSP character can send SIGTSTP to the foreground group, and an interactive shell can later resume that job. A one-shot remote command has no useful interactive job-control conversation just because it owns a PTY. Suspending it can leave the SSH client waiting on a stopped remote group with no shell prompt from which to run fg.
Do not make a remote transaction safe by assuming the operator can press Ctrl-C at the right moment. Put cancellation semantics in the remote command. A shell wrapper can trap signals and terminate its child group; a service manager can own the process; a remote timeout can bound execution. Then test the disconnect case, not only the keyboard case.
ssh -tt host.example '
trap '\''printf "wrapper got INT\n" >&2; exit 130'\'' INT
printf "remote shell pid=%s\n" "$$"
sleep 300
'
Pressing Ctrl-C should exercise the PTY path and return control. Repeat with the real program because shells, terminal applications, and process supervisors install different handlers. Also test closing the network connection. PTY teardown can generate hangup behavior for the controlling session, while a no-PTY child that retains an output pipe can keep the SSH channel open. Backgrounding a process is not a detachment plan in either mode.
For long work, submit it to a remote service manager or job system and return a job identifier. That gives cancellation an explicit remote target and lets SSH report only whether submission succeeded. It also prevents a brief client outage from deciding whether half of a migration continues.
Exit status survives, but wrappers can replace it
OpenSSH returns the remote command's exit status in both modes. Its client manual says ssh exits with that status, or 255 when an error occurs. RFC 4254 describes the exit-status channel message as a 32 bit unsigned value and a separate exit-signal message for signal termination.
PTY allocation does not make a successful status more trustworthy. Shell composition determines which status becomes the remote command status. This command reports the status of printf, so it can hide a failed deployment:
ssh -tt host.example 'deploy; printf "finished\n"'
Preserve the result deliberately:
ssh -T host.example '
deploy
rc=$?
printf "deploy_status=%s\n" "$rc" >&2
exit "$rc"
'
rc=$?
printf 'ssh_status=%s\n' "$rc"
exit "$rc"
Local pipelines can hide it again. In shells without a pipeline status option, ssh host command | tee log commonly returns the status of tee. Capture SSH output to a file first, use a shell with a tested pipeline status feature, or read the pipeline's per-command statuses. Do not assume set -e repairs every pipeline, conditional, or subshell.
Status 255 is ambiguous because OpenSSH reserves it for client errors while a remote program can also exit 255. If that distinction matters, have the remote wrapper emit a structured completion record on a protected channel or map application statuses into an agreed range. Authentication failure, host verification failure, lost transport, and a literal remote 255 should not all trigger the same retry.
Connection setup failures happen before a remote wrapper can print anything. Use client timeouts and enough SSH diagnostics to classify those locally, but keep diagnostics away from the program's data file. Once the server starts the wrapper, include a run identifier and a final status record so a caller can distinguish "never started" from "started, then connection lost." That distinction prevents blind retries of commands that may already have changed state.
Signal deaths need the same care. A shell often represents a signal as 128 plus the signal number, but the SSH protocol can report an exit signal directly, and client behavior need not look like a local shell's child status in every case. Define the statuses your wrapper emits instead of reverse engineering all values above 128 after an outage.
A PTY merges streams and edits bytes
Requesting a PTY gives up clean separation between remote standard output and standard error. OpenSSH's server implementation duplicates the PTY slave onto descriptors 0, 1, and 2. The protocol has an extended data channel for standard error, but a process attached to one terminal has already written both output streams into the same byte stream.
This breaks a common automation pattern:
ssh -T host.example command >result.json 2>diagnostic.log
With no PTY, the remote program's standard error can reach diagnostic.log while JSON stays in result.json. With a PTY, remote warnings, password prompts, banners, progress displays, and JSON can arrive together in result.json; local SSH diagnostics can still appear in diagnostic.log. Redirection on the client cannot unmix bytes after the server merged them.
Terminal output processing may also convert newline to carriage return plus newline. Input can be echoed, canonical mode can wait for a line, and control characters can invoke terminal functions. These are correct terminal behaviors and corruptions in a binary protocol.
Terminal-aware programs often add color, progress bars, pagers, or prompts. They may line buffer output on a terminal but block buffer it through a pipe, which makes a no-PTY run look stuck even when it is working. Fix buffering at the application when possible: choose its plain output flag, unbuffered mode, or logging option. A fake terminal changes several variables at once and can conceal the actual buffering issue.
If a command produces machine data, keep -T and make the program emit a noninteractive format. If it truly needs a terminal, treat its entire output as a transcript. Do not parse a PTY transcript as though it were a stable API.
Agent-run SSH must fail without conversation
An autonomous caller cannot safely own a terminal conversation. A prompt that helps a human recover can make an agent wait, improvise input, or misclassify a partial action as success. The SSH command should declare whether interaction is allowed before credentials, privilege, or remote state enter the picture.
For agent runs, combine no PTY with prompt suppression at each layer. SSH host authentication and host verification need prearranged policy. Privilege escalation should use sudo -n. The remote tool should receive its noninteractive flag, an explicit timeout, and inputs through files or named arguments rather than a conversational prompt. Capture standard output, standard error, and status as separate results.
Do not solve a missing password prompt by exposing a password or private key to the agent. Authentication material and terminal behavior are separate concerns. Giving the process a PTY does not reduce what stolen credentials can do, and withholding a PTY does not protect credentials already present in its environment.
Sallyport takes the credential boundary out of the agent process: an MCP capable agent requests an SSH action through the bundled stateless sp-ssh helper, while the encrypted vault supplies the SSH key and the Activity journal records the call. The command still needs an intentional PTY contract because secret isolation cannot decide whether a remote program expects terminal semantics.
Approval is also different from a remote prompt. A local approval card can authorize a defined action before execution; a remote terminal prompt appears inside an open session and may arrive after earlier side effects. Keep human authorization outside the remote byte stream so denial has a clear meaning and logs can identify the attempted action.
When an agent needs an interactive maintenance tool, split the workflow. Let the agent prepare a command or request, then hand the live terminal session to a human, or replace the tool with a noninteractive operation designed for automation. Pretending an agent is a typist produces the least testable version of both systems.
Choose the mode as part of the interface
The right mode follows from the remote program's interface, not from a general rule that PTYs are good or bad. Use this decision table during review:
- Use
-Tfor JSON, archives, and exact bytes because it preserves transparent data and separate error output. - Use
-Twithsudo -nfor unattended privileged work so authentication fails instead of waiting. - Use
-twhen a human will type a sudo password or operate a terminal interface. - Use
-ttfrom a pipe or agent launcher only after reviewing the missing local TTY override. - Submit long work to a remote manager because a PTY does not provide durable process ownership.
Record the choice beside the command. Tests should assert descriptor types, stream separation, cancellation, and status, not only a few expected output words. If a dependency upgrade starts requiring a terminal, fail the test and investigate the new prompt or detection branch before adding -t.
Include the selected mode in logs as structured metadata rather than inferring it from colored output or a prompt. When an incident crosses several hosts, that single field lets an operator separate terminal behavior from authentication, network, and application failures before rerunning anything.
Server policy can refuse PTYs through OpenSSH configuration or an authorized key restriction. That is another reason not to make them an accidental dependency. A command built for automation should remain usable under -T; an interactive command should fail clearly when it cannot acquire a terminal.
Treat any proposed -tt change as an interface change. Re-run the probe, redirect both local streams, send Ctrl-C, force the remote command to return a nonzero status, and disconnect mid-run. If the result cannot be described precisely, the command is not ready to run unattended.
FAQ
What does ssh -t actually do?
It asks the SSH server to allocate a PTY and attach the remote session to it. Programs then see terminal descriptors, remote output streams merge, and terminal input rules can generate signals.
What is the difference between ssh -t and ssh -tt?
-t forces a PTY request when the local client has a terminal. Repeating it as -tt also forces allocation when local standard input is not a terminal, which is common in pipelines and agent launchers.
Does SSH allocate a TTY for a remote command by default?
Normally, a specified remote command runs without a PTY. Use -T to make that choice explicit or -t when the command genuinely needs a terminal.
Why does sudo fail over SSH without a TTY?
When sudo needs a password, it normally reads from the user's terminal. For automation, use sudo -n and an appropriately narrow sudoers rule so the command fails instead of prompting.
Is sudo -S safe in an SSH script?
sudo -S makes password transport the script's responsibility and mixes the secret into standard input. Avoid it for agents and routine automation; use a nonprompting authorization design instead.
Does ssh -t load .bashrc or .profile?
No, PTY allocation does not select shell startup files. Request a login or interactive shell explicitly if required, though an explicit environment and executable path produce more predictable jobs.
Why are stdout and stderr mixed with ssh -t?
OpenSSH attaches descriptors 1 and 2 to the same PTY slave, so the server has one terminal byte stream to send. Client redirection cannot reconstruct the original streams afterward.
Does a PTY change the SSH exit code?
The PTY itself does not; OpenSSH still returns the remote command status. Shell wrappers, pipelines, signal termination, and the special client error status 255 can change or obscure what the caller observes.
Why does Ctrl-C behave differently with and without ssh -t?
A PTY terminal driver converts the interrupt character into SIGINT for its foreground process group. Without a PTY, there is no remote terminal foreground group, so cancellation depends on SSH signal support and process structure.
Should AI agents use ssh -tt for commands that might prompt?
No. An agent command should suppress prompts and fail with a useful status; forcing a terminal can turn a configuration error into an indefinite conversation and merge output needed for diagnosis.