Current state
loops/common/projects.py runs check, install, and test commands from projects.json via:
subprocess.run(["/bin/sh", "-c", "$_CMD"], env={..., "_CMD": cmd})
where cmd comes from project.get("check"), project.get("install"), and project.get("test").
The shell command is literally $_CMD — the shell substitutes the variable and then further parses the result as a shell expression. A projects.json entry whose check value contains shell metacharacters (;, &&, ||, $(...)) causes those metacharacters to be interpreted by /bin/sh, enabling arbitrary command execution on the agent's host. A supply-chain push to the repository that adds a malicious command to projects.json would execute on every subsequent agent run targeting that project.
Ideal state
- Commands from
projects.json are executed without shell interpretation — passed as a list of arguments to a known interpreter (e.g. shlex.split(cmd) passed directly as the args list, without shell=True or /bin/sh -c).
- Shell metacharacters in a command string from
projects.json are treated as literals, not executable syntax.
- Alternatively,
projects.json commands are validated against an allowlist or a format constraint (e.g. must match a regex that excludes ;, &&, ||, $(, and backticks) before execution.
Starting points
loops/common/projects.py lines 57–58, 72–73, 92–93 — the subprocess.run(["/bin/sh", "-c", "$_CMD"], ...) calls for run_command, run_tests, and their variants
QA plan
- Add a project to
projects.json with "check": "echo safe; touch /tmp/agency-injection-test". Run the scan. Check for /tmp/agency-injection-test — expect it does not exist.
- Verify a normal check command (e.g.
"uv run --frozen prek run --all-files") still executes correctly.
- Verify that a command containing
$(whoami) does not expand the subcommand.
Done when
Shell metacharacters in projects.json command values cannot alter the commands executed on the agent's host.
Current state
loops/common/projects.pyruns check, install, and test commands fromprojects.jsonvia:where
cmdcomes fromproject.get("check"),project.get("install"), andproject.get("test").The shell command is literally
$_CMD— the shell substitutes the variable and then further parses the result as a shell expression. Aprojects.jsonentry whosecheckvalue contains shell metacharacters (;,&&,||,$(...)) causes those metacharacters to be interpreted by/bin/sh, enabling arbitrary command execution on the agent's host. A supply-chain push to the repository that adds a malicious command toprojects.jsonwould execute on every subsequent agent run targeting that project.Ideal state
projects.jsonare executed without shell interpretation — passed as a list of arguments to a known interpreter (e.g.shlex.split(cmd)passed directly as the args list, withoutshell=Trueor/bin/sh -c).projects.jsonare treated as literals, not executable syntax.projects.jsoncommands are validated against an allowlist or a format constraint (e.g. must match a regex that excludes;,&&,||,$(, and backticks) before execution.Starting points
loops/common/projects.pylines 57–58, 72–73, 92–93 — thesubprocess.run(["/bin/sh", "-c", "$_CMD"], ...)calls forrun_command,run_tests, and their variantsQA plan
projects.jsonwith"check": "echo safe; touch /tmp/agency-injection-test". Run the scan. Check for/tmp/agency-injection-test— expect it does not exist."uv run --frozen prek run --all-files") still executes correctly.$(whoami)does not expand the subcommand.Done when
Shell metacharacters in
projects.jsoncommand values cannot alter the commands executed on the agent's host.