Drop quotesu/ setup doc; make the demo take any sicd host as an argument

The quotesu setup notes exposed internal host details and aren't useful to
readers, so remove the directory. The quoting-hell demo now requires the
target host as an argument (any host running sicd) instead of defaulting to
an internal name, and its labels say sic rather than the old rs. Example
hostnames in the README/SKILL/main.go are now the neutral placeholder host1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
This commit is contained in:
Markus Fritsche
2026-07-19 16:03:42 +02:00
parent 35a8a07152
commit 99568666d3
5 changed files with 41 additions and 101 deletions
+4 -4
View File
@@ -44,10 +44,10 @@ sic <host> -- <command> [arg ...] # explicit separator
```
```
sic quotesu echo hello world
sic quotesu touch 'a b' # one file named "a b"
sic quotesu echo '$HOME' # literal, no expansion
sic --sh quotesu 'echo hi | wc -c'
sic host1 echo hello world
sic host1 touch 'a b' # one file named "a b"
sic host1 echo '$HOME' # literal, no expansion
sic --sh host1 'echo hi | wc -c'
```
## Build
+5 -5
View File
@@ -22,11 +22,11 @@ Pass each argument as a separate argument to `sic`. Do not construct netstrings;
`sic` frames them.
```
sic quotesu echo hello world
sic quotesu touch 'my file.txt'
sic quotesu echo '$HOME' # literal, no expansion
sic quotesu python3 -c 'import sys; print(sys.argv)' a 'b c'
sic --sh quotesu 'grep foo *.log | wc -l'
sic host1 echo hello world
sic host1 touch 'my file.txt'
sic host1 echo '$HOME' # literal, no expansion
sic host1 python3 -c 'import sys; print(sys.argv)' a 'b c'
sic --sh host1 'grep foo *.log | wc -l'
```
## exec vs --sh
+3 -3
View File
@@ -11,9 +11,9 @@
//
// Examples:
//
// sic quotesu echo hello world
// sic quotesu touch 'a b' # ONE file, not two
// sic --sh quotesu 'echo hi | wc -c'
// sic host1 echo hello world
// sic host1 touch 'a b' # ONE file, not two
// sic --sh host1 'echo hi | wc -c'
package main
import (
+29 -27
View File
@@ -3,18 +3,20 @@
Each scenario is run both ways:
A) Plain SSH — shows the breakage
B) sic tool — shows the fix
B) sic — shows the fix
Usage:
python3 demo-quoting-hell.py <host>
Point it at any host that runs sicd (reachable over SSH with your key):
python3 quoting-hell.py <host>
"""
import subprocess
import sys
import shlex
import tempfile
import os
HOST = sys.argv[1] if len(sys.argv) > 1 else "quotesu"
if len(sys.argv) < 2:
sys.exit("usage: python3 quoting-hell.py <host> # any host running sicd")
HOST = sys.argv[1]
SIC = os.path.expanduser("~/bin/sic")
SSH = ["/usr/bin/ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5"]
@@ -29,7 +31,7 @@ def ssh(cmd_str):
def sic(*args):
"""Run argv via sic tool (which frames netstrings for sicd)."""
"""Run argv via sic (which frames netstrings for sicd)."""
return subprocess.run(
[SIC, HOST] + list(args),
capture_output=True, text=True, timeout=10
@@ -62,7 +64,7 @@ def result(label, r):
# ── 1. Space in filename ──────────────────────────────────────────
section("1. Space in filename — the classic")
sub("SSH: ssh host touch 'my file.txt'")
sub(f"SSH: ssh {HOST} touch 'my file.txt'")
# SSH concatenates args with spaces, remote shell re-splits
r = ssh("touch 'my file.txt'")
r2 = ssh("ls my file.txt 2>&1; echo ---; ls 'my file.txt' 2>&1")
@@ -72,10 +74,10 @@ print(" → 'touch' sees TWO args: 'my' and 'file.txt' — two files created"
# Clean up
ssh("rm -f my file.txt 'my file.txt'")
sub("rs: rs quotesu touch 'my file.txt'")
sub(f"sic: sic {HOST} touch 'my file.txt'")
r = sic("touch", "my file.txt")
r2 = sic("ls", "-la", "my file.txt")
result("rs", r2)
result("sic", r2)
print("'touch' sees ONE arg: 'my file.txt' — one file created")
sic("rm", "-f", "my file.txt")
@@ -83,59 +85,59 @@ sic("rm", "-f", "my file.txt")
# ── 2. Dollar sign / variable expansion ───────────────────────────
section("2. Dollar sign — unintended variable expansion")
sub("SSH: ssh host 'echo \$HOME'")
sub(f"SSH: ssh {HOST} 'echo \$HOME'")
r = ssh("echo $HOME")
result("SSH", r)
print(" → Remote shell expands $HOME — leaks local values")
sub("SSH escaped: ssh host 'echo \\$HOME'")
sub(f"SSH escaped: ssh {HOST} 'echo \\$HOME'")
r = ssh("echo \\$HOME")
result("SSH escaped", r)
sub("rs: rs quotesu echo '\$HOME'")
sub(f"sic: sic {HOST} echo '\$HOME'")
r = sic("echo", "$HOME")
result("rs", r)
result("sic", r)
print(" → No shell involved — literal $HOME printed")
# ── 3. Backticks / command substitution ──────────────────────────
section("3. Backticks — unintended command execution")
sub("SSH: ssh host 'echo `hostname`'")
sub(f"SSH: ssh {HOST} 'echo `hostname`'")
r = ssh("echo `hostname`")
result("SSH", r)
print(" → Backticks execute on remote — prints quotesu")
print(" → Backticks execute on remote — prints the remote hostname")
sub("rs: rs quotesu echo '`hostname`'")
sub(f"sic: sic {HOST} echo '`hostname`'")
r = sic("echo", "`hostname`")
result("rs", r)
result("sic", r)
print(" → Literal backticks — no execution")
# ── 4. Nested quotes ─────────────────────────────────────────────
section("4. Nested quotes — escaping hell")
sub('SSH: ssh host "echo \\"hello world\\""')
sub(f'SSH: ssh {HOST} "echo \\"hello world\\""')
r = ssh('echo "hello world"')
result("SSH", r)
sub('rs: rs quotesu echo "hello world"')
sub(f'sic: sic {HOST} echo "hello world"')
r = sic("echo", "hello world")
result("rs", r)
result("sic", r)
print(" → No nested quoting needed — just pass the string")
# ── 5. Newlines in arguments ──────────────────────────────────────
section("5. Newlines in arguments")
sub("SSH: ssh host 'echo \"line1\\nline2\"'")
sub(f"SSH: ssh {HOST} 'echo \"line1\\nline2\"'")
r = ssh('echo "line1\nline2"')
result("SSH", r)
print(" → Newline in SSH command string is fragile")
sub("rs: rs quotesu echo 'line1\\nline2'")
sub(f"sic: sic {HOST} echo 'line1\\nline2'")
r = sic("echo", "line1\nline2")
result("rs", r)
result("sic", r)
print(" → Newline is just a byte in a netstring field")
@@ -149,7 +151,7 @@ r = ssh(script)
result("SSH", r)
print(" → Try to get all those quoting levels right...")
sub("rs: rs quotesu python3 -c ... with clean args")
sub("sic: sic <host> python3 -c ... with clean args")
r = sic(
"python3", "-c",
"import sys; [print(i, repr(a)) for i,a in enumerate(sys.argv)]",
@@ -160,7 +162,7 @@ r = sic(
"arg with `ls`",
'nested "quotes"'
)
result("rs", r)
result("sic", r)
print(" → Each argument is a separate field — no escaping needed")
@@ -169,7 +171,7 @@ section("SUMMARY")
print("""
The pattern in every case:
SSH → flattens argv to one string, shell re-parses → QUOTING HELL
rs → sends argv as framed fields, execvp directly → NO ESCAPING
sic → sends argv as framed fields, execvp directly → NO ESCAPING
Netstrings (length:bytes,) are the key: the length tells the parser
exactly where each field ends, so NO delimiter needs escaping.
-62
View File
@@ -1,62 +0,0 @@
# quotesu — test target for sicd
Incus container on `dcw2` (192.168.88.114), created as the target host for
sicd development and quoting-hell demonstrations.
## Host facts
| Property | Value |
|---|---|
| Host machine | `dcw2` (192.168.88.114, aarch64, Debian 13) |
| Container IP | 192.168.88.196/24 |
| Network | `br0` (unmanaged bridge) |
| Storage | `usb` pool (btrfs, /mnt/usb-noether) |
| OS | Debian 13 "trixie" (arm64) |
| Incus version | 6.0.4 |
| SSH user | `mfritsche` (key auth + passwordless sudo) |
## Setup steps
```sh
# 1. Create container
incus init debian-13 quotesu --storage usb --network br0
incus start quotesu
# 2. Install SSH + user
incus exec quotesu -- apt-get update -qq
incus exec quotesu -- apt-get install -y -qq openssh-server sudo python3
incus exec quotesu -- adduser --disabled-password --gecos "" mfritsche
incus file push ~/.ssh/authorized_keys quotesu/home/mfritsche/.ssh/authorized_keys
incus exec quotesu -- chown -R mfritsche:mfritsche ~mfritsche/.ssh
incus exec quotesu -- chmod 700 ~mfritsche/.ssh
incus exec quotesu -- chmod 600 ~mfritsche/.ssh/authorized_keys
incus exec quotesu -- adduser mfritsche sudo
echo "mfritsche ALL=(ALL) NOPASSWD:ALL" | \
incus exec quotesu -- tee -a /etc/sudoers.d/mfritsche
incus exec quotesu -- systemctl restart ssh
# 3. Deploy sicd
incus exec quotesu -- mkdir -p /opt/sicd
# Copy ../gateway/sicd to the container, then:
incus exec quotesu -- chmod 755 /usr/local/bin/sicd
incus exec quotesu -- ln -sf /usr/local/bin/sicd /usr/local/bin/sicd
```
## Verification
```sh
# Test the gateway
printf '4:exec,2:id,0:,' | ssh quotesu sicd
# → uid=1000(mfritsche) gid=1000(...
```
## DNS note
dcw2 had a broken `/etc/resolv.conf` (NetworkManager header, no nameservers).
Fixed by setting:
```
nameserver 1.1.1.1
nameserver 192.168.88.1
```
This was needed for `incus image copy images:debian/13 local:...` to resolve
the image server hostname.