Quoting Across Local and Remote Shell Boundaries
Remote shell commands can be parsed twice: once locally and once remotely. Reliable automation begins by deciding which parser owns each expansion and by keeping arbitrary data out of executable shell source.
Learning objectives
By the end of this lesson
- Identify local and remote parsing boundaries.
- Control where variable expansion occurs.
- Pass data as arguments or stdin.
- Avoid remote command injection.
- Recognize when nested quoting should be redesigned.
1. Remote commands may pass through multiple parsers
flowchart LR L["local Bash parser"] --> S["ssh argument transport"] S --> R["remote shell parser"] R --> C["remote command"]
A string can be interpreted once by the local shell and again by the remote shell. Most SSH quoting bugs come from forgetting which shell should expand which variable.
2. Decide whether expansion belongs locally or remotely
local_value='from-local'
ssh host "printf '%s\n' '$local_value'"Here the local shell expands $local_value before SSH runs. That can be correct for trusted local data, but it is easy to create injection bugs with arbitrary input.
3. Single quotes can defer expansion to the remote shell
ssh host 'printf "remote HOME=%s\n" "$HOME"'The local shell passes the dollar sign literally; the remote shell expands $HOME.
4. Prefer passing data as arguments over constructing remote shell source
service='api worker'
printf -v remote_cmd 'printf "service=%%s\\n" %q' "$service"
ssh host "$remote_cmd"Bash printf %q can produce shell-escaped text for Bash-like remote shells, but this is still shell-source construction and depends on the remote shell grammar.
5. Send a script over stdin for complex remote logic
ssh -T host 'bash -s -- arg1 arg2' <<'REMOTE'
set -u
first=$1
second=$2
printf 'first=%s second=%s\n' "$first" "$second"
REMOTEA quoted here-document delimiter prevents the local shell from expanding the script body. Positional arguments provide a cleaner data channel.
6. Environment forwarding is policy-controlled and limited
SSH can forward selected environment variables only when both client and server permit it. Do not assume arbitrary environment state crosses the connection automatically.
7. Arbitrary data must not become remote code
# Dangerous design:
# ssh host "rm -rf /srv/apps/$user_supplied_name"
# Better: validate data and pass it as an argument to a fixed remote program.
ssh host /usr/local/bin/remove-app "$validated_name"A variable that becomes part of remote shell source can become command injection. Prefer fixed remote commands plus validated arguments.
8. Nested quotes are a signal to simplify the design
If a command contains multiple layers of backslashes, single quotes, double quotes, command substitutions, and embedded JSON, stop and move logic into a script or structured data file.
9. Structured payloads should stay structured
payload=$(jq -n --arg service "$service" '{service:$service}')
printf '%s\n' "$payload" |
ssh host 'jq -r .service'This sends JSON as data through stdin rather than embedding it inside remote command syntax.
10. Remote paths with unusual characters deserve conventions
A technically valid remote filename containing quotes or newlines can make SSH command construction extremely difficult. Production environments benefit from constrained naming rules for deployable paths and service identifiers.
11. Hands-on lab: compare local and remote expansion
mkdir -p "$HOME/devops-academy/bash/chapter14/lesson04"
cd "$HOME/devops-academy/bash/chapter14/lesson04"
cat > quoting-notes.sh <<'EOF'
#!/usr/bin/env bash
set -u
host=${1:-}
[[ -n $host ]] || {
printf 'usage: %s HOST\n' "$0" >&2
exit 64
}
local_value='LOCAL VALUE'
printf '%s\n' '--- local expansion ---'
ssh -T "$host" "printf '%s\n' '$local_value'"
printf '%s\n' '--- remote expansion ---'
ssh -T "$host" 'printf "remote HOME=%s\n" "$HOME"'
printf '%s\n' '--- script over stdin ---'
ssh -T "$host" 'bash -s -- "arg with spaces"' <<'REMOTE'
set -u
printf 'remote arg=%s\n' "$1"
REMOTE
EOF
chmod u+x quoting-notes.sh
printf 'Run with a reachable SSH host to compare expansion boundaries.\n'Verification checklist
12. Knowledge check
Question 1. Why are SSH quoting bugs common?
Question 2. How can you defer $HOME expansion to the remote shell?
Question 3. What is safer than embedding arbitrary data into remote shell source?
Question 4. When should nested quoting be replaced by another design?
13. Summary
Remote-shell quoting is about parser ownership. Decide which shell should expand each token, keep arbitrary data out of command source, use positional arguments or stdin for complex workflows, and simplify whenever nested quoting becomes difficult to audit.
14. Further reading
- GNU Bash Reference Manual — Quoting.
- OpenSSH
ssh(1)manual. - OpenSSH environment-forwarding configuration.
- ShellCheck guidance — quoting and SSH command construction.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.