Skip to content

The shell provider cannot run a process directly: argv execution is absent #801

Open

Description

@David-Noble-at-work

Part of #799.

Every command must currently pass through a shell. shell.exec builds sh -c <string>, so a
caller with a program and its arguments has to flatten them into one string and hope the shell
reassembles them the way it meant.

That is a correctness boundary, not a convenience. A command string is parsed: quoting, word
splitting, globbing, and substitution all apply, and every caller must defend against them. argv is
handed to execve as a vector that nothing reinterprets.

The launchers this epic serves make the case concretely — they build commands from issue titles and
branch names, which are user-supplied text containing spaces, quotes and occasionally a $. Under
a shell string every one of those is a hazard the caller must escape. Under argv none of them are.

It is also a dependency boundary: direct execution needs no shell present and no shell chosen, which
matters on a host where the preferred shell is missing or wrong.

Shape

pkg/process.Runner takes a prepared *exec.Cmd and is indifferent to how it was built, so this
is a thin action over the same machinery #800 routes command execution through:

// run a process directly; nothing reinterprets argv
func (p *Provider) Run(argv []string) (*Result, error)

Named shell.run(argv), settled in #799: run waits, exec replaces, and a _command suffix
means a shell parses the input. The four cells are run / run_command / exec / exec_command,
following the convention file already sets with read_text / read_bytes.

shell.run is anchored in subprocess.run(["git", "clone"]), which is the argv form a
Python-literate reader already writes — and which is why Gemini's analogy to subprocess.run for the
shell-string action was inverted.

Depends on #800 in practice rather than in principle — both build a *exec.Cmd and hand it to
Runner, and doing them separately means writing that seam twice.

Plan and design documents

Kind Document
Plan not yet — the plan arrives when the branch does
Design draft in hand, uncommitted; lands with the first worktree

Epic: #815 · Feature: #799

Activity

  1. added 2 commits that reference this issue on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Epic:ProvidersThe provider surface: one feature per providertaskImplementation work item

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions