sic — quoting-proof remote command execution
Client (sic) frames argv as netstrings and pipes it over ssh to a daemon (sicd) that execvp's it, so no shell re-parses the arguments. exec mode by default; opt-in sh mode for pipes/redirects/globs. Go client + daemon, Python reference daemon, design doc, quoting-hell demo, and foreground/background agent skills. Renamed from the climcp prototype. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
// sic — frame argv as netstrings and pipe to sicd on a remote host.
|
||||
//
|
||||
// Each argument is framed as a netstring and written to sicd's stdin over ssh.
|
||||
// The netstring framing is done here so callers pass argv on the command line
|
||||
// and never construct frames by hand.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// sic [--sh] <host> <command> [<arg> ...]
|
||||
// sic [--sh] <host> -- <command> [<arg> ...] (explicit separator)
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// sic quotesu echo hello world
|
||||
// sic quotesu touch 'a b' # ONE file, not two
|
||||
// sic --sh quotesu 'echo hi | wc -c'
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// netstring encodes a UTF-8 string as a netstring (djb format).
|
||||
func netstring(s string) string {
|
||||
encoded := []byte(s)
|
||||
return strconv.Itoa(len(encoded)) + ":" + string(encoded) + ","
|
||||
}
|
||||
|
||||
// frameArgv builds a single netstring command frame from args.
|
||||
func frameArgv(args []string, mode string) []byte {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(netstring(mode))
|
||||
switch mode {
|
||||
case "exec":
|
||||
for _, a := range args {
|
||||
sb.WriteString(netstring(a))
|
||||
}
|
||||
case "sh":
|
||||
sb.WriteString(netstring(strings.Join(args, " ")))
|
||||
}
|
||||
sb.WriteString("0:,")
|
||||
return []byte(sb.String())
|
||||
}
|
||||
|
||||
func main() {
|
||||
argv := os.Args[1:]
|
||||
|
||||
// Parse flags
|
||||
mode := "exec"
|
||||
for len(argv) > 0 && strings.HasPrefix(argv[0], "--") {
|
||||
switch argv[0] {
|
||||
case "--sh":
|
||||
mode = "sh"
|
||||
case "--":
|
||||
argv = argv[1:]
|
||||
goto parseArgs
|
||||
}
|
||||
argv = argv[1:]
|
||||
}
|
||||
|
||||
parseArgs:
|
||||
if len(argv) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Usage: sic [--sh] <host> <command> [<arg> ...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Locate host and command
|
||||
var host string
|
||||
var command []string
|
||||
|
||||
if idx := indexOf(argv, "--"); idx >= 0 {
|
||||
host = argv[0]
|
||||
command = argv[idx+1:]
|
||||
} else if len(argv) >= 2 {
|
||||
host = argv[0]
|
||||
command = argv[1:]
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "sic: missing command arguments")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
frameData := frameArgv(command, mode)
|
||||
sshCmd := exec.Command("ssh", "-o", "StrictHostKeyChecking=no", host, "sicd")
|
||||
sshCmd.Stdin = strings.NewReader(string(frameData))
|
||||
sshCmd.Stdout = os.Stdout
|
||||
sshCmd.Stderr = os.Stderr
|
||||
if err := sshCmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
os.Exit(exitErr.ExitCode())
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "sic: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func indexOf(slice []string, s string) int {
|
||||
for i, v := range slice {
|
||||
if v == s {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// sicd — receive netstring-framed argv over stdin, execvp it directly.
|
||||
//
|
||||
// Deployed on target hosts. Reads netstrings from stdin, interprets the first
|
||||
// field as a mode selector ("exec" or "sh"), and runs the command accordingly.
|
||||
//
|
||||
// Wire format (netstrings, djb):
|
||||
//
|
||||
// <mode-netstring> <arg-netstring>* <empty-netstring>
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ssh <host> sicd < /some/netstring/frames
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// readNetstring reads one netstring from buf. Returns (payload, rest, ok).
|
||||
func readNetstring(buf []byte) ([]byte, []byte, bool) {
|
||||
i := bytes.IndexByte(buf, ':')
|
||||
if i < 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
n, err := strconv.Atoi(string(buf[:i]))
|
||||
if err != nil || n < 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
start := i + 1
|
||||
end := start + n
|
||||
if len(buf) < end+1 || buf[end] != ',' {
|
||||
return nil, nil, false
|
||||
}
|
||||
return buf[start:end], buf[end+1:], true
|
||||
}
|
||||
|
||||
// runFrame executes a single command frame.
|
||||
func runFrame(mode string, args [][]byte) int {
|
||||
switch mode {
|
||||
case "exec":
|
||||
argv := make([]string, len(args))
|
||||
for i, a := range args {
|
||||
argv[i] = string(a)
|
||||
}
|
||||
if len(argv) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "sicd: exec mode with no args")
|
||||
return 1
|
||||
}
|
||||
cmd := exec.Command(argv[0], argv[1:]...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "sicd: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case "sh":
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "sicd: sh mode with no args")
|
||||
return 1
|
||||
}
|
||||
shellCmd := string(args[0])
|
||||
cmd := exec.Command("sh", "-c", shellCmd)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "sicd: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "sicd: unknown mode %q\n", mode)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
buf, err := os.ReadFile("/dev/stdin")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "sicd: read stdin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var mode string
|
||||
var fields [][]byte
|
||||
framesProcessed := 0
|
||||
|
||||
for {
|
||||
field, rest, ok := readNetstring(buf)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
buf = rest
|
||||
|
||||
if mode == "" {
|
||||
mode = string(field)
|
||||
} else if len(field) == 0 {
|
||||
// Empty netstring = end of frame
|
||||
if mode != "" {
|
||||
runFrame(mode, fields)
|
||||
framesProcessed++
|
||||
}
|
||||
mode = ""
|
||||
fields = fields[:0]
|
||||
} else {
|
||||
fields = append(fields, field)
|
||||
}
|
||||
}
|
||||
|
||||
if framesProcessed == 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user