Skip to content

runtime: backport POSIX single-quote escaping to non-K8s runtimes (Docker/Podman/Apple) #162

@jduncan-rva

Description

@jduncan-rva

Summary

The tmux command-string construction in pkg/runtime/common.go:379-386 uses an incomplete character set when deciding whether to shell-quote harness args. Tokens containing backticks, parens, braces, brackets, backslashes, !, or glob characters (*, ?, ~) fall through unquoted and get re-interpreted by the shell when the tmux new-session string is parsed. This manifests as silent prompt mangling when task strings contain ordinary markdown.

A correct fix already exists elsewhere in the repo — pkg/runtime/k8s_runtime.go:2016-2020 uses POSIX single-quote escaping (''"'"') for the K8s su -c path, and a comment at k8s_runtime.go:791 notes that this was done to "avoid shell quoting issues with the tmux command string." That fix never propagated to the Docker, Podman, or Apple Container runtimes, which all share the pkg/runtime/common.go code path.

Impact

Observed in a real dispatch: a task prompt containing backtick-wrapped file paths (standard markdown — `apps/backend/src/templates/notificationTemplates.ts`) triggered zsh inside the tmux session to execute each backtick as command substitution:

zsh:1: permission denied: apps/backend/src/templates/notificationTemplates.ts
zsh:1: command not found: 0xD32F2F

The harness received a prompt with the backtick-wrapped sections replaced by error strings / empty strings. The agent silently executed a mangled instruction — no error surfaced to the dispatcher, the run just produced confused output. This is the worst failure mode for autonomous systems: looks like it worked, didn't actually work.

The same class of failure is reachable with any task string containing parens (clarifications), $ (prices, env refs — currently caught), globs (file patterns), or embedded shell metacharacters.

Current code

pkg/runtime/common.go:379-386:

for _, a := range harnessArgs {
    if strings.ContainsAny(a, " \t\n\"'$") {
        quotedArgs = append(quotedArgs, fmt.Sprintf("%q", a))
    } else {
        quotedArgs = append(quotedArgs, a)
    }
}

Two problems with this:

  1. Character set is incomplete. strings.ContainsAny misses backticks, (, ), {, }, [, ], \, !, *, ?, ~. Any of those in a bare token survives un-quoted and gets shell-interpreted.
  2. %q is Go's syntax, not shell's. Go's %q uses Go escape conventions (e.g., \n, \t, \xNN) that are not identical to POSIX shell quoting. Double-quoted strings in shell still evaluate $, backticks, and \ — so even when quoting is applied, command substitution inside the token isn't fully neutralized.

Proposed fix

Mirror the K8s path's POSIX single-quote escaping. Always single-quote (don't gate on character-set detection), and handle embedded single-quotes with the standard '"'"' pattern:

func posixShellQuote(s string) string {
    return "'" + strings.ReplaceAll(s, "'", `'"'"'`) + "'"
}

for _, a := range harnessArgs {
    quotedArgs = append(quotedArgs, posixShellQuote(a))
}

POSIX single quotes disable all shell interpretation inside the token — no command substitution, no variable expansion, no glob, no backslash escapes. This is the standard idiom and it's already what k8s_runtime.go:2019 does.

Benefits:

  • Covers every metacharacter, including ones not yet considered.
  • Matches the K8s path — one quoting implementation across all runtimes.
  • Cheap: always-quote adds a few bytes to the tmux command string and costs nothing at runtime.
  • No behavior change for well-formed prompts; only previously-broken prompts start working correctly.

Acceptance

  • pkg/runtime/common.go:379-386 uses POSIX single-quote escaping for harness args.
  • A unit test in pkg/runtime/common_test.go exercises task strings containing: backticks, $foo, $(whoami), parens, braces, globs (*.ts), embedded single quotes, embedded double quotes, newlines, and a markdown-ish realistic example. The test asserts the harness receives the exact bytes passed in (no shell re-interpretation).
  • Behavior verified against all three runtimes (Docker, Podman, Apple Container) — quoting is runtime-agnostic but worth running the integration path in each.
  • K8s path already uses the correct quoting; no change needed there.

Out of scope

  • Any redesign of how prompts are passed (positional vs. file vs. stdin). This bug-fix issue is purely about correctness of the existing inline path.
  • Escape-char handling inside the harness itself (Claude/Gemini/Codex prompt parsing) — that's downstream of this.

Context

Branch main, commit 22dfe46a. No prior issue or PR touches this escaping path; the comment at k8s_runtime.go:791 is the closest thing to acknowledgment in the codebase.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions