Skip to content

Harden loop-action command execution against unquoted shell expansion - #395

Merged
cobusgreyling merged 2 commits into
cobusgreyling:mainfrom
amarjaleelbanbhan:claude/github-issue-393-loop-action-cl7atd
Jul 27, 2026
Merged

Harden loop-action command execution against unquoted shell expansion#395
cobusgreyling merged 2 commits into
cobusgreyling:mainfrom
amarjaleelbanbhan:claude/github-issue-393-loop-action-cl7atd

Conversation

@amarjaleelbanbhan

@amarjaleelbanbhan amarjaleelbanbhan commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

inputs.command was spliced as raw text into the run: script before bash parsed it. For multi-line command: | blocks this silently broke sandbox isolation: only the first line reached loop-sandbox run -- ...; every later line executed as a bare top-level statement on the runner, bypassing the sandbox and circuit-breaker entirely.

Write command to a temp script (passed via env:, never re-templated by the workflow YAML) and invoke it as a single quoted bash "$SCRIPT" call from both the sandbox and direct paths. Document safe usage and warn against embedding untrusted input in command in the README.

Summary

Changes

  • New pattern or starter (followed templates/pattern-template.md + updated registry.yaml)
  • Doc / example improvement
  • Tool change (loop-audit)
  • Story (includes real failure or surprise + lesson)

Checklist (from CONTRIBUTING)

  • All required sections present for patterns
  • Links work from README, patterns/README, starters/README, docs/index
  • No secrets, tokens, internal company URLs
  • STATE.md* examples use .example suffix
  • Safety-related content references docs/safety.md
  • Ran node tools/loop-audit/dist/cli.js . (or on the starter) and addressed findings

Testing / Dogfood

  • loop-audit passes on affected starters or this repo
  • Manual review of generated state / skill output

Screenshots / Examples (if UI or command output)


This template enforces the high bar this reference is known for.

inputs.command was spliced as raw text into the run: script before bash
parsed it. For multi-line command: | blocks this silently broke sandbox
isolation: only the first line reached loop-sandbox run -- ...; every
later line executed as a bare top-level statement on the runner,
bypassing the sandbox and circuit-breaker entirely.

Write command to a temp script (passed via env:, never re-templated by
the workflow YAML) and invoke it as a single quoted bash "$SCRIPT" call
from both the sandbox and direct paths. Document safe usage and warn
against embedding untrusted input in command in the README.

@cobusgreyling cobusgreyling left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR correctly fixes the real multi-line/inputs.command splicing bug: moving command through env: into a temp script and invoking a single bash "$LOOP_SCRIPT_PATH" keeps the whole payload inside sandbox/direct execution and avoids unquoted expansion in the outer run: script. The approach matches GitHub’s recommended pattern, and the README’s untrusted-input warning is accurate. The main remaining correctness gap is that the nested bash invocation does not use Actions’ default -eo pipefail, so multi-line scripts can continue after failures and even report success if the last line succeeds.

Issue counts by severity

  • bugs: 1
  • suggestions: 1
  • nits: 1

Decision: request changes. The temp-script approach is the right fix, but please invoke scripts with bash -eo pipefail (both sandbox and direct paths) so multi-line command matches Actions fail-fast behavior before merge.

Comment thread tools/loop-action/action.yml Outdated
run: ${{ inputs.command }}
env:
LOOP_SCRIPT_PATH: ${{ steps.write_command.outputs.script-path }}
run: bash "$LOOP_SCRIPT_PATH"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] The direct path runs bash "$LOOP_SCRIPT_PATH" (and the sandbox path does the same via bash "$LOOP_SCRIPT_PATH" on lines 57/59). GitHub Actions’ default bash shell is effectively bash --noprofile --norc -eo pipefail {0}, but a nested bash /path/to/script starts a fresh shell without -e or pipefail. For the multi-line command: | support this PR advertises, that means (1) a failing intermediate line does not stop later lines, and (2) the step’s exit code is only the last command’s status — so false / echo done succeeds overall. That is a behavior footgun relative to a normal run: step and can hide agent failures when a later line succeeds.

Suggestion: Invoke the temp script with the same fail-fast options Actions uses, on both paths, e.g. bash -eo pipefail "$LOOP_SCRIPT_PATH" (or bash --noprofile --norc -eo pipefail "$LOOP_SCRIPT_PATH"). Apply that in the direct run: line and in both sandbox loop-sandbox run ... -- bash ... invocations.

Comment thread tools/loop-action/action.yml Outdated
if [ "${{ inputs.sandbox-shell }}" == "true" ]; then
npx --yes @cobusgreyling/loop-sandbox run --shell -- ${{ inputs.command }}
if [ "$LOOP_SANDBOX_SHELL" == "true" ]; then
npx --yes @cobusgreyling/loop-sandbox run --shell -- bash "$LOOP_SCRIPT_PATH"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] After this change, command is always executed as a bash script (bash <script>), so sandbox-shell: 'true' only adds an extra Node shell: true wrap around an already-shell-driven bash invocation. The flag is now largely redundant for loop-action callers (including the multi-line examples that previously needed --shell for shell syntax). Keeping it for compatibility is fine, but the dual branch suggests a semantic difference that no longer really exists for this action.

Suggestion: Either always call loop-sandbox run -- bash -eo pipefail "$LOOP_SCRIPT_PATH" (drop the branch), or keep the branch but document in the README inputs table that command is always run via bash <temp-script>, so sandbox-shell is optional/compatibility-only when using loop-action.

Comment thread tools/loop-action/action.yml Outdated
run: |
script_path="$(mktemp "${RUNNER_TEMP:-/tmp}/loop-action-command.XXXXXX")"
printf '%s\n' "$LOOP_COMMAND" > "$script_path"
chmod +x "$script_path"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] chmod +x "$script_path" is unnecessary because every execution path invokes the file as bash "$LOOP_SCRIPT_PATH" rather than executing it directly. Harmless, but it implies the script is meant to be exec’d on its own (which would also want a shebang).

Suggestion: Remove the chmod +x line, or if you prefer direct execution later, add a #!/usr/bin/env bash header when writing the file and exec "$script_path" instead.

Run the temporary command script with bash -eo pipefail on the sandbox
and direct paths so an intermediate failure in a multi-line command
stops execution immediately and the step returns non-zero, instead of
continuing on to later lines. Drop the chmod +x on the script since
it's always invoked as an explicit argument to bash and never executed
directly. Document sandbox-shell as a loop-sandbox compatibility option
rather than something required for shell syntax in command.
@amarjaleelbanbhan

Copy link
Copy Markdown
Contributor Author

Implemented the requested changes in ff8378b:

  • Added bash -eo pipefail to all execution paths.
  • Removed unnecessary chmod +x.
  • Documented the sandbox-shell compatibility behavior.
  • Added fail-fast and pipefail coverage for direct and sandbox execution.

All relevant local tests pass.

@cobusgreyling cobusgreyling left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (post-fix commit ff8378b)

All three review items addressed:

  1. bugbash -eo pipefail on both sandbox and direct paths restores Actions fail-fast / pipeline semantics for multi-line command: | blocks
  2. suggestionsandbox-shell documented as a loop-sandbox compatibility knob, not a gate for shell syntax
  3. nit — unnecessary chmod +x removed

Temp-script via env: + single quoted path remains the right fix for the original unquoted multi-line splice / sandbox-bypass bug. README warning on untrusted input in command is correct and important.

Decision: approve. Ready to merge.

@cobusgreyling cobusgreyling left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (triage)

Thanks @amarjaleelbanbhan — follow-up commit ff8378b addresses the prior change requests:

  • Scripts now run with bash -eo pipefail on both sandbox and direct paths
  • Unnecessary chmod +x removed
  • README documents sandbox-shell as a loop-sandbox compatibility knob and documents fail-fast / multi-line behavior

Decision: approve + merge.

@cobusgreyling

Copy link
Copy Markdown
Owner

Maintainer note (PR triage 2026-07-27)

Approved — review items from the prior pass are fixed on ff8378b (bash -eo pipefail, drop chmod +x, document sandbox-shell).

Fork workflows were stuck in action_required after reopen (required for audit/validate under branch protection). I approved the pending runs and also opened same-repo PR #404 with the same commits so merge isn't blocked if the fork approval path is flaky.

If #404 merges first, this PR will be closed as superseded with full attribution to @amarjaleelbanbhan.

@cobusgreyling
cobusgreyling merged commit b3908a3 into cobusgreyling:main Jul 27, 2026
4 checks passed
@cobusgreyling

Copy link
Copy Markdown
Owner

Merged — thank you @amarjaleelbanbhan. Important sandbox-isolation fix for multi-line command: | blocks, plus fail-fast semantics and clear untrusted-input guidance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants