actions-run-one-line accepts an inlined program on one line, defeating its own stated intent #989

Closed
opened 2026-08-10 02:28:21 +00:00 by coilyco-ops · 1 comment
Owner

The gap

actions-run-one-line's own description says:

Reject multiline run commands in GitHub and Forgejo workflows and composite actions. Move script bodies into tracked language-native files.

The checker enforces the first sentence and not the second. A step can inline an arbitrarily large program as long as it is one physical line, so the documented intent is unenforced.

Evidence

In coilyco-bridge/deploy, 24 of 25 workflows carried an identical ~50-line Python program escaped into a single line:

run: "python3 -c 'exec(\"import os\\nimport sys\\nimport urllib.error\\n...\")'"

That passes the hook today. It is unreadable, and unreachable by ruff, shellcheck, review, and git diff.

Two concrete costs observed there:

  1. Drift. A body inlined in YAML cannot be shared, so it was copy-pasted 24 times. One copy had already diverged: it lost continue-on-error, lost the missing-secret guard, lost proxy support, and reported the wrong failure kind. Nothing flagged it, because each copy is locally valid.
  2. The workaround is the path of least resistance. An agent hitting the hook will reach for -c 'exec(...)' rather than create a tracked file, because the former satisfies the counter immediately. That is how 24 files got there.

Ask

Extend the checker so a run: that inlines a program body fails, not just a multiline one. A workable heuristic without being clever about it:

  • reject -c, -e, or -E immediately followed by a long inline source string, for python/python3, node, ruby, perl
  • reject an inline body over a character threshold, or one containing escaped newlines (\n) in the source string
  • reject a heredoc opened and closed within the single line

The bar to encode is roughly "the line names a file to execute, or is a short direct command," not "the line contains a program."

Prior art for the target shape

coilyco-bridge/deploy PR #338 sweeps its 25 workflows to the stricter rule and writes it up in docs/actions-step-shape.md. Three shared scripts replaced the three inlined bodies. The parsed workflow semantics were verified identical before and after, so the sweep is mechanical and safe to copy.

Done when

A workflow step inlining a program body through python3 -c 'exec(...)' fails actions-run-one-line, and the fleet's existing instances are either fixed or explicitly tracked.

## The gap `actions-run-one-line`'s own description says: > Reject multiline run commands in GitHub and Forgejo workflows and composite actions. **Move script bodies into tracked language-native files.** The checker enforces the first sentence and not the second. A step can inline an arbitrarily large program as long as it is one physical line, so the documented intent is unenforced. ## Evidence In `coilyco-bridge/deploy`, **24 of 25 workflows** carried an identical ~50-line Python program escaped into a single line: ```yaml run: "python3 -c 'exec(\"import os\\nimport sys\\nimport urllib.error\\n...\")'" ``` That passes the hook today. It is unreadable, and unreachable by `ruff`, `shellcheck`, review, and `git diff`. Two concrete costs observed there: 1. **Drift.** A body inlined in YAML cannot be shared, so it was copy-pasted 24 times. One copy had already diverged: it lost `continue-on-error`, lost the missing-secret guard, lost proxy support, and reported the wrong failure kind. Nothing flagged it, because each copy is locally valid. 2. **The workaround is the path of least resistance.** An agent hitting the hook will reach for `-c 'exec(...)'` rather than create a tracked file, because the former satisfies the counter immediately. That is how 24 files got there. ## Ask Extend the checker so a `run:` that inlines a program body fails, not just a multiline one. A workable heuristic without being clever about it: * reject `-c`, `-e`, or `-E` immediately followed by a long inline source string, for `python`/`python3`, `node`, `ruby`, `perl` * reject an inline body over a character threshold, or one containing escaped newlines (`\n`) in the source string * reject a heredoc opened and closed within the single line The bar to encode is roughly "the line names a file to execute, or is a short direct command," not "the line contains a program." ## Prior art for the target shape `coilyco-bridge/deploy` PR #338 sweeps its 25 workflows to the stricter rule and writes it up in `docs/actions-step-shape.md`. Three shared scripts replaced the three inlined bodies. The parsed workflow semantics were verified identical before and after, so the sweep is mechanical and safe to copy. ## Done when A workflow step inlining a program body through `python3 -c 'exec(...)'` fails `actions-run-one-line`, and the fleet's existing instances are either fixed or explicitly tracked.
Author
Owner

PR #990 implements this. workflow: pull-request-and-merge, so it is open and not merged.

Checker. A run: now also fails when it passes an inline source string to python, node, ruby, perl, or a shell through -c, -e, -E, -p, --eval, --exec, or --print and that string carries an escaped newline or exceeds MAX_INLINE_BODY_CHARS (100), or when it opens a heredoc. Short direct one-liners still pass, as do printf 'a\nb\n', herestrings, and $(( 1 << 3 )). Bodies are measured exactly via shlex, with a tail-regex fallback when the command quoting is unbalanced. Both paths are tested.

This repo's instances. 12 steps across 6 workflows, all byte-identical, now call ./actions/telegram-alert rather than a new script. The repo already shipped that composite action with a tested telegram_alert.py and a one-line run:, so the 12 copies were simply the unmigrated half. Parsed workflow semantics verified identical apart from those step bodies.

Fleet tracking. Every remaining instance is filed:

  • coilyco-bridge/deploy - 24, already swept by deploy#338
  • coilyco-flight-deck/infrastructure#775 - 5
  • coilyco-bridge/agentic-os-kai#852 - 4
  • coilyco-bridge/agentic-os-hardware#90 - 1
  • coilyco-bridge/agentic-os-xxx#26 - 1

Each bites only when that repo bumps its aos-precommit ref.

One nit left open. The alert message headline is the literal CI failed on main even on steps guarded by refs/heads/release, in both this repo's composite action and deploy's script. I left it alone rather than fork the two programs mid-sweep. Worth a small follow-up once deploy#338 lands and the two can change together.

PR #990 implements this. `workflow: pull-request-and-merge`, so it is open and not merged. **Checker.** A `run:` now also fails when it passes an inline source string to `python`, `node`, `ruby`, `perl`, or a shell through `-c`, `-e`, `-E`, `-p`, `--eval`, `--exec`, or `--print` and that string carries an escaped newline or exceeds `MAX_INLINE_BODY_CHARS` (100), or when it opens a heredoc. Short direct one-liners still pass, as do `printf 'a\nb\n'`, herestrings, and `$(( 1 << 3 ))`. Bodies are measured exactly via `shlex`, with a tail-regex fallback when the command quoting is unbalanced. Both paths are tested. **This repo's instances.** 12 steps across 6 workflows, all byte-identical, now call `./actions/telegram-alert` rather than a new script. The repo already shipped that composite action with a tested `telegram_alert.py` and a one-line `run:`, so the 12 copies were simply the unmigrated half. Parsed workflow semantics verified identical apart from those step bodies. **Fleet tracking.** Every remaining instance is filed: * `coilyco-bridge/deploy` - 24, already swept by deploy#338 * `coilyco-flight-deck/infrastructure#775` - 5 * `coilyco-bridge/agentic-os-kai#852` - 4 * `coilyco-bridge/agentic-os-hardware#90` - 1 * `coilyco-bridge/agentic-os-xxx#26` - 1 Each bites only when that repo bumps its `aos-precommit` ref. **One nit left open.** The alert message headline is the literal `CI failed on main` even on steps guarded by `refs/heads/release`, in both this repo's composite action and deploy's script. I left it alone rather than fork the two programs mid-sweep. Worth a small follow-up once deploy#338 lands and the two can change together.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agentic-os#989
No description provided.