Surviving Hostile Remote Shells
A complaint I get to make almost every working day: the text I paste into a remote shell is not the text that arrives. Remote support means my commands rarely travel a clean path from my keyboard to the target system. They go through an RDP clipboard, or a screen-sharing session's remote control, or a browser-based console, sometimes two of those chained together, and every hop is an opportunity for the text to come out the other side subtly wrong.
The failure everyone learns first is the collapsed paste. You copy a tidy three-line block, paste it, and the remote side receives it with the line breaks stripped, so three separate commands arrive smashed together as one long line that either errors out or, much worse, runs as something you did not intend. The mechanics vary by path. Windows and Unix disagree about line endings (CRLF versus LF), and clipboard bridges translate between them with varying competence, so line breaks get dropped, doubled, or converted into stray carriage returns that show up as ^M. Screen-share remote control often does not paste at all in the clipboard sense; it replays your text as synthetic keystrokes, and under latency, keystrokes get reordered or lost. Web-based out-of-band consoles are the worst offenders in my experience, typing your paste out character by character and quietly dropping some of them when the buffer cannot keep up. A dropped character in the middle of a path is an error message. A dropped character in the middle of a flag can be a behavior change.
So the habits below all serve one principle: write commands that remain correct even after the transport has done its worst.
The collapse you can see and the collapse you cannot
Sort the damage by how loudly it announces itself, because that sorting is what tells you where to spend effort.
Some collapses are loud. Two commands run together produce a syntax error, the shell refuses, nothing happens, and you paste again more carefully. That is a wasted minute and a mild embarrassment on a screen share.
The collapses worth defending against are the quiet ones, where the joined text is still valid shell and still runs. Consider a three-line block that prepares a directory:
mkdir -p /srv/export
touch /srv/export/.marker
chown svc_app /srv/export
Strip the newlines and the shell sees mkdir -p /srv/export touch /srv/export/.marker chown svc_app /srv/export. That is a single valid mkdir invocation with five path arguments. It creates five directories, one of them named touch, one named chown, one named svc_app, and it exits zero. No error, no complaint, and the marker file and the ownership change never happened. You move on believing the step succeeded, and the thing that eventually fails is three steps downstream at a point where nobody is looking at this paste anymore.
That is the shape to keep in mind. The dangerous transport failure produces a successful-looking command that did something else.
Make the line breaks explicit, and choose them deliberately
If line breaks might be stripped, stop depending on them. Put the separators in the text itself, and pick the one that says what you mean. A semicolon runs the next command no matter what happened to the previous one. Double ampersand runs the next command only if the previous one succeeded, and in operational work that difference is not stylistic, it is the whole ballgame:
cd /var/log/application; rm -f *.old
cd /var/log/application && rm -f *.old
The first version deletes files wherever you happen to be standing if the cd fails. The second refuses. When a command sequence includes anything destructive, the connector is a safety decision, and my default in remote sessions is && unless I have a specific reason to want unconditional continuation.
The choice is rarely uniform across a whole block, which is the part that takes some thought. A validation sequence that mounts something, reads from it, and cleans up wants both operators in the same paste:
temporary_mount="$(mktemp -d)" &&
mount -t nfs -o ro,soft,timeo=30,retrans=2 storage01.example.net:/export/data "$temporary_mount" &&
head -c 4096 "$temporary_mount/known_file.txt" > /dev/null;
umount "$temporary_mount";
rmdir "$temporary_mount";
The mount and the read are chained with && so a failed mount never reaches a read that would report success against an empty local directory. The cleanup uses ; so a failed read still gets the mount torn down instead of leaving it behind for the next attempt to trip over. Ask of every line ending what should happen if the line above it failed, and the operator writes itself.
One more property of that block is worth noticing. Every line ends in an operator and no line ends in a backslash, which means the sequence runs in the same order whether it arrives as five lines or one. Collapsing it is no longer a failure mode. It is just a different rendering of the same command.
Terminate the last line too
The last line of a pasted block looks like it needs nothing. There is no following line for it to collide with, so a terminator there does no work.
It does work one block later. Paste a block, have the trailing newline stripped in transit, and the final command sits in the input buffer unsubmitted, waiting for an Enter that the paste did not deliver. Nothing looks wrong: the terminal shows what appears to be a completed paste. Then you paste the next block, and its first line lands on the end of the previous one. The same collision, one block boundary later, and this time you are less likely to catch it because you already watched the first paste go in cleanly.
A terminator on the final line costs one character and removes the boundary. It is always a semicolon there, never &&, because a trailing && leaves the shell waiting for an operand it will never get.
Where a terminator cannot go
The rule has a limit, and knowing where it stops is what keeps it from producing worse output than it prevents.
A terminator attaches to a completed command. Shell keywords that require the next line to finish a construct reject one outright. then, do, else, and in all take the following line as part of the same statement, and a semicolon after any of them is a syntax error. The same is true of a line ending in { or (, a case branch pattern ending in ), a line ending in a pipe or && or ||, and a backslash continuation, where the semicolon becomes an argument rather than a separator.
Block closers behave the opposite way. fi, done, esac, and } all complete a construct, so they take a terminator and it is worth putting one there, because those lines end a logical unit and are exactly where a following paste would land.
Heredocs are their own case and the one most likely to bite. Everything between <<EOF and the closing EOF is data being fed to a command, so a semicolon added to one of those lines becomes part of the text you are writing to a file rather than a separator. That matters here because the heredoc is also the tool recommended below for landing a paste somewhere inert, which means the safety mechanism and the terminator habit have to coexist without one corrupting the other. Put the terminator on the line that opens the heredoc if it needs one, and leave the body alone.
Which means "put a semicolon on every line" is not implementable, and a block written that way will not parse. The workable rule is narrower: every line that is a completed command, plus the closers, plus the last line. Everything the shell needs to continue is left alone.
Respect the backslash, and fear its trailing space
For long commands that genuinely need visual line breaks, backslash continuation works, with one trap that has burned everyone who uses it: the backslash must be the very last character on the line. A single invisible space after it turns "continue this line" into "escape this space," the continuation breaks, and the next line executes as its own command. Pasting is exactly how those invisible trailing spaces get introduced.
The deeper problem is that backslash continuation depends on the newline surviving, which is the one thing a hostile transport will not promise you. Strip the newline and the backslash escapes the space that follows it, and that escaped space becomes an argument. A curl invocation split across four lines for readability:
curl --fail --retry 5 \
--retry-delay 2 \
"$url"
arrives with newlines intact as four arguments, --fail, --retry, 5, $url, which is what you wrote. Arrives collapsed, it becomes six:
argv[1] = [--fail]
argv[2] = [ ]
argv[3] = [--retry]
argv[4] = [5]
argv[5] = [ ]
argv[6] = [$url]
The bare-space arguments are the escaped spaces. curl will refuse that, loudly, which is the saving grace: this failure announces itself rather than silently doing the wrong thing. But it is still a failed command in front of a customer, and the fix is free.
My rule: backslash continuations are for scripts in files, where an editor shows me trailing whitespace and no clipboard is involved. For interactive paste into a hostile path, I one-line with explicit separators instead, or I accept the long line.
Know whether your shell will catch you
Modern bash has a quiet safety net worth knowing about. Since bash 5.1 (readline 8.1), bracketed paste mode is on by default: the terminal wraps pasted text in escape markers, and the shell inserts the whole paste into the edit buffer without executing anything, even if the paste contains newlines. You get to read what actually arrived, then press Enter once. That final review is the single best defense in this whole article, and it is worth confirming with bind -v | grep bracketed that it is on.
Then remember the two limits. Older shells on older appliance and enterprise systems do not have it, which describes plenty of what support engineers actually connect to. And paths that replay keystrokes rather than pasting bypass it entirely, because the shell never sees a paste at all, only a fast typist.
The habit that works everywhere: paste into something inert first. The remote side's editor, a cat > /tmp/commands.txt heredoc, anything that lets you inspect what survived the trip before any of it can execute. On an unfamiliar system, that inspection is also how you find out which of the two limits above you are dealing with.
Watch for text that was poisoned before you copied it
The transport is not the only saboteur. If a command spent time in an email, a Word document, or a ticketing system before you copied it, assume a word processor has "improved" it: straight quotes replaced with curly ones, double hyphens fused into a dash, sometimes non-breaking spaces where spaces should be. All of it looks nearly identical on screen and none of it is valid shell syntax, which produces error messages pointing at characters that appear perfectly fine.
The tell is a syntax error whose column indicator lands on something obviously correct. When a pasted command fails that way, retype the quotes and dashes by hand before doing anything else. This fix has a hit rate that embarrasses most of my cleverer theories.
The structural version of the fix is to stop routing commands through applications that reformat text. A runbook stored as plain text in a repository arrives intact. The same runbook pasted into a change ticket and copied back out arrives with three characters silently replaced.
The discipline underneath all of it
Every habit here is one idea wearing different clothes: never let a hostile transport be the thing that decides what executes. Make separators explicit so structure survives collapse. Choose && where a failure should stop the train and ; where cleanup has to run anyway. Terminate the last line so the next paste has nothing to land on. Land the paste somewhere inert and read it before it runs.
There is a verification half to this that the paste habits do not cover. Every defense above protects the text on its way in. None of them tells you the command did what you meant once it ran, and the quiet collapse in the first example exits zero, so the exit status will not raise a hand either. After any paste that changed state on a remote system, check the state rather than the return code: list the directory you created, read back the file you wrote, confirm the service is in the state you moved it to. On a system you reached through three hops and will disconnect from in a minute, that read-back is the last chance anyone gets to notice.
On a support call with a customer's production system on the other end of the session, the pause to verify the paste going in and the state coming out is cheap insurance, and unlike most insurance it pays out several times a week.