<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>automation &#8211; Flugphase</title>
	<atom:link href="https://blog.reauktion.de/tag/automation/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.reauktion.de</link>
	<description>Die Gedanken sind frei</description>
	<lastBuildDate>Sun, 19 Jul 2026 12:34:42 +0000</lastBuildDate>
	<language>de</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>
	<item>
		<title>sic: remote commands without the SSH quoting hell</title>
		<link>https://blog.reauktion.de/sic-ssh-quoting-hell/</link>
					<comments>https://blog.reauktion.de/sic-ssh-quoting-hell/#respond</comments>
		
		<dc:creator><![CDATA[Markus Fritsche]]></dc:creator>
		<pubDate>Sun, 19 Jul 2026 12:12:17 +0000</pubDate>
				<category><![CDATA[IT]]></category>
		<category><![CDATA[ai-agents]]></category>
		<category><![CDATA[automation]]></category>
		<category><![CDATA[execvp]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[mcp]]></category>
		<category><![CDATA[netstrings]]></category>
		<category><![CDATA[quoting]]></category>
		<category><![CDATA[ssh]]></category>
		<guid isPermaLink="false">https://blog.reauktion.de/?p=2875</guid>

					<description><![CDATA[If you drive a fleet of machines over SSH &#8212; or you are an AI agent that does &#8212; you have hit this bug, probably today: ssh host touch 'my file.txt' That does not create one file called my file.txt. It creates two: my and file.txt. The quotes protected the argument on your local shell,...]]></description>
										<content:encoded><![CDATA[<p>If you drive a fleet of machines over SSH &mdash; or you are an AI agent that does &mdash; you have hit this bug, probably today:</p>
<pre><code>ssh host touch 'my file.txt'</code></pre>
<p>That does not create one file called <code>my file.txt</code>. It creates two: <code>my</code> and <code>file.txt</code>. The quotes protected the argument on your local shell, then SSH discarded them and handed <code>touch my file.txt</code> to a <em>remote</em> shell, which split it again on the space.</p>
<p>SSH does not take an argument vector. It takes a string and runs it through <code>sh -c</code> on the far end. Every quote, <code>$</code>, backtick, glob and space you send is re-parsed by a shell whose quoting you did not write. One hop is annoying. Two hops &mdash;</p>
<pre><code>ssh gateway 'ssh inner "grep $pattern *.log"'</code></pre>
<p>&mdash; is a combinatorial mess of backslashes, and the moment a value contains a space, an apostrophe, or a curly quote pasted from somewhere, it breaks in a way that is tedious to debug and easy to get subtly wrong. Delete the wrong file this way once and you stop finding it funny.</p>
<h2>The usual workarounds, and why they only move the pain</h2>
<ul>
<li><strong>Escape harder.</strong> <code>printf %q</code> survives one hop. Two hops means escaping the escaping. It does not compose.</li>
<li><strong>base64.</strong> Encode the command, decode on the far side, pipe to <code>sh</code>. It works, but every command is now an opaque blob and you are still running it through a shell.</li>
<li><strong>Here-docs over stdin.</strong> Ties up stdin and still shell-parses the result.</li>
</ul>
<p>All of these treat the symptom. The disease is that the argument vector &mdash; the thing the kernel actually wants, a list of byte strings &mdash; gets flattened into one string and re-split by a shell at every hop.</p>
<h2>sic: keep the argv a vector</h2>
<p><strong>sic</strong> never flattens the argument vector. The client frames each argument as a <a href="https://cr.yp.to/proto/netstrings.txt">netstring</a> (<code>&lt;length&gt;:&lt;bytes&gt;,</code>), pipes the frame over SSH to a small daemon, <code>sicd</code>, which reads the frame and calls <code>execvp()</code> directly. No shell re-parses the arguments on the far end. What you passed is exactly what runs.</p>
<pre><code>sic host touch 'my file.txt'      # one file, correctly named
sic host echo '$HOME'             # prints the literal string $HOME
sic host python3 -c 'print(1+1)'  # no quoting gymnastics</code></pre>
<p>Because netstrings are length-prefixed, there is nothing to escape: a byte is a byte. Spaces, quotes, dollar signs, newlines and UTF-8 all survive untouched, however many hops deep you go.</p>
<h2>When you actually want a shell</h2>
<p>Sometimes the pipe, the glob or the redirect is the point. sic does not take that away &mdash; it just makes it explicit:</p>
<pre><code>sic --sh host 'grep foo *.log | wc -l'</code></pre>
<p>With <code>--sh</code>, sic hands the string to <code>sh -c</code> on purpose. Without it you get <code>execvp</code> and no surprises. The default is the safe one.</p>
<h2>Why it exists</h2>
<p>sic was built for AI agents driving machine fleets, where the quoting hell is constant and a wrong split can mean the wrong file removed. It is deliberately smaller than standing up a per-host <a href="https://modelcontextprotocol.io/">MCP</a> server: one static Go binary as the daemon, pinned to an SSH key with <code>command="sicd"</code>, plus a client that is just argv-to-netstrings. If you already have SSH to a box, you already have the transport.</p>
<p>Security model: <code>sicd</code> execs whatever it is handed, so it must only ever be reachable over an authenticated SSH channel &mdash; never exposed on a TCP port &mdash; and is best pinned to a dedicated key.</p>
<h2>Get it</h2>
<pre><code>git clone https://github.com/marfrit/sic
# or: git clone https://git.reauktion.de/marfrit/sic
cd sic
go build -o bin/sic  ./cmd/sic
go build -o bin/sicd ./cmd/sicd</code></pre>
<p>Copy <code>sicd</code> to each target host (a static <code>CGO_ENABLED=0</code> build runs anywhere, musl included), pin it to a key, and use <code>sic &lt;host&gt; &lt;command&gt; [args&hellip;]</code> wherever you used to write <code>ssh &lt;host&gt; '&hellip;'</code>. MIT licensed.</p>
<p>Repositories: <a href="https://github.com/marfrit/sic">github.com/marfrit/sic</a> and <a href="https://git.reauktion.de/marfrit/sic">git.reauktion.de/marfrit/sic</a>.</p>
<p>One shameless ask: if sic just saved you an afternoon of backslash archaeology, star the <a href="https://github.com/marfrit/sic">repo on GitHub</a>. It never phones home, so a star is the only way I find out anyone actually runs it.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.reauktion.de/sic-ssh-quoting-hell/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
