Skip to content

fix: caller metadata with a line break can no longer inject workflow commands into package publish logs - #3447

Merged
Patrick-Erichsen merged 1 commit into
openclaw:mainfrom
Yigtwxx:fix/package-publish-log-newline
Aug 11, 2026
Merged

fix: caller metadata with a line break can no longer inject workflow commands into package publish logs#3447
Patrick-Erichsen merged 1 commit into
openclaw:mainfrom
Yigtwxx:fix/package-publish-log-newline

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Related: #3414

What Problem This Solves

A repository that publishes packages through the reusable package-publish.yml workflow can pass a
changelog, categories or topics value. If that value contains a line break, the workflow's own
run log stops being one line and the GitHub Actions runner executes the remainder as a workflow
command: the caller can set an annotation, and the same channel reaches ::error, ::warning,
::add-mask and ::stop-commands.

The values are caller-controlled workflow_call inputs, so anything that composes this workflow —
a matrix job, a release automation, a downstream repository passing a changelog straight from a tag
message or a PR body — can carry a newline into the log without anyone intending it.

Why This Change Was Made

The resolve step builds a shell line and both writes it to a re-runnable .sh file and echoes it:

shell_line = " ".join(shlex.quote(part) for part in cmd)
path.write_text("#!/usr/bin/env bash\nset -euo pipefail\n" + shell_line + "\n", encoding="utf-8")
path.chmod(0o755)
print(shell_line)

shlex.quote is shell quoting, not output escaping. Given "a\n::notice::x" it returns
'a\n::notice::x' — single quotes around a line break that is still a line break. That is correct
for the .sh file, where a shell reads the quotes, and wrong for the echo, where the runner reads
each stdout line on its own.

So the file keeps shell_line untouched and only the echo changes, through a quote_for_log
helper that falls back to json.dumps when the shell-quoted form is not printable:

quoted = shlex.quote(part)
return quoted if quoted.isprintable() else json.dumps(part)

Two choices inside that:

  1. str.isprintable(), not an explicit \r\n check. It is false for every C0/C1 control
    character and for the Unicode line and paragraph separators, so nothing has to enumerate the
    characters that can split a line. --changelog 'Adds 日本語 notes' is printable and stays on the
    readable, copy-pasteable shlex.quote path; only a value that cannot be printed as one line is
    escaped.
  2. json.dumps, not a blanket repr. ensure_ascii defaults to true, so the fallback output
    is ASCII-only and cannot smuggle a separator back in.

This is the same fix as #3414 on the skill workflow, where the finding was raised. I noticed the
package side while writing that one; it has the same shape because the two workflows forward the
same three caller-controlled inputs.

User Impact

Callers of the reusable package publish workflow can no longer influence the runner through
metadata inputs, intentionally or by accident. Nothing else changes: the executed command is a
subprocess.run argument list and is untouched, the generated .sh file is byte-identical, and a
metadata value with no control characters logs exactly as it did before.

Evidence

Real behavior. Two jobs in one dispatch, same multi-line changelog, differing only in the
pinned ClawHub SHA — 82313c2b (current main) and 25aec929 (this branch's head). Both hardcode
dry_run: true and pass no secrets:
(run 31409524372,
workflow source):

changelog: |-
  Line one of the changelog value.
  ::notice title=INJECTED::this line came from a changelog input

before-fix
— the echo breaks in two and the runner consumes the second line:

bun ... package publish Yigtwxx/clawhub-skill-metadata-proof@15655db6 ... --dry-run --json --tags latest --changelog 'Line one of the changelog value.
##[notice]this line came from a changelog input' --source-ref refs/heads/main

after-fix
— one line, and the payload is inert text:

bun ... package publish Yigtwxx/clawhub-skill-metadata-proof@15655db6 ... --dry-run --json --tags latest --changelog "Line one of the changelog value.\n::notice title=INJECTED::this line came from a changelog input" --source-ref refs/heads/main

The check-run annotations are the unambiguous half, because a line the runner accepts as a command
is removed from the log rather than printed:

$ gh api repos/.../check-runs/93523961368/annotations   # before-fix
failure   -          Process completed with exit code 1.
warning   -          Plugin Inspector skipped because ... is not a plugin root.
notice    INJECTED   this line came from a changelog input' --source-ref refs/heads/main

$ gh api repos/.../check-runs/93523961426/annotations   # after-fix
failure   -          Process completed with exit code 1.
warning   -          Plugin Inspector skipped because ... is not a plugin root.

The injected annotation carries the tail of the real command, which is what the runner swallowed.

Both jobs end red, and that is expected. The step under test, Resolve publish command, is
green in both; the caller repository holds no ClawPack package, so both jobs then stop at
Run package publish with the identical exit code 1 and the identical plugin-inspector warning
above. Those two annotations appear on both sides and cancel out; the INJECTED one does not.

One thing worth a maintainer's eye, which the fix does not and cannot address: the raw second line
is also in both jobs' logs before the resolve step runs, in the runner's own ##[group] Inputs
echo of the reusable-workflow inputs and in the env: block printed above each step. Those are the
runner writing its own log rather than step stdout, so they are never parsed — the after-fix job
having no INJECTED annotation is the proof of that. A multi-line input stays visible in logs
either way; what this fix removes is the part where it becomes executable.

Tests. src/__tests__/package-publish-workflow.test.ts gains a case that pins the helper
itself, that the echo goes through it, and that shell_line keeps plain shell quoting for the .sh
file:

bunx vitest run src/__tests__/package-publish-workflow.test.ts scripts/security/package-publish-workflow.test.ts
  7 passed (7)

Negative control: restoring only .github/workflows/package-publish.yml from main gives
1 failed | 5 passed, so the test fails without the fix. The test pins the fallback rather than
only the absence of the old call, so replacing quote_for_log with anything that leaves a control
character intact fails it.

All seven embedded Python blocks in the workflow were extracted from the file and parsed with
ast.parse; all seven are syntactically valid, including the 243-line block this touches.

bunx oxfmt --check src/__tests__/package-publish-workflow.test.ts    All matched files use the correct format.

Pre-existing CI failure, unrelated to this branch. pr-gates currently fails at bun audit on
main itself and therefore on every open PR; #3446 fixes that separately. It is not caused by this
diff.

Screenshots: N/A, workflow change with no UI surface.

The resolve step echoes the publish command with shlex.quote, which is shell
quoting rather than output escaping: it wraps a value holding a line break in
single quotes and leaves the break itself intact. A caller-supplied changelog,
categories or topics value carrying a newline therefore opened a second line
in the step log, and the runner parses each stdout line, so that second line
reached it as a workflow command.

Escape the parts that are not printable in the echo. The re-runnable .sh file
keeps plain shell quoting, because there the quoting is what makes the script
correct.
@Yigtwxx
Yigtwxx requested a review from a team as a code owner August 10, 2026 16:34
@clawsweeper

clawsweeper Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@Yigtwxx is attempting to deploy a commit to the OpenClaw Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@clawsweeper clawsweeper Bot added P2 Normal backlog priority with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Aug 10, 2026
@clawsweeper

clawsweeper Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed August 11, 2026, 10:22 AM ET / 14:22 UTC.

ClawSweeper review

What this changes

The PR escapes non-printable caller metadata in the reusable package-publish workflow’s command log so it cannot be parsed as a GitHub Actions workflow command.

Regression provenance

Possible regression — probable (reproduction; reviewed change). No predecessor PR is attributed.

Merge readiness

⚠️ Ready for maintainer review - 2 items remain

Keep open: current main retains the affected log behavior, while this is a focused, proof-positive hardening patch with no blocking correctness findings.

Priority: P2
Reviewed head: 25aec92957aac15b7dc719c0dbc5c56e1c4b5d0f

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) Strong real workflow evidence and a narrow, well-contained patch support normal maintainer merge review.
Proof confidence 🦞 diamond lobster (5/6) ✨ media proof bonus Sufficient (linked_artifact): The PR supplies a linked live reusable-workflow before/after run at pinned baseline and branch SHAs, including the disappearance of the injected check annotation.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (linked_artifact): The PR supplies a linked live reusable-workflow before/after run at pinned baseline and branch SHAs, including the disappearance of the injected check annotation.
Evidence reviewed 5 items Current main still exposes the behavior: Current main accepts caller-controlled changelog, categories, and topics values into the command list, then prints a shell-quoted command line; shell quoting preserves embedded newlines in stdout.
The affected workflow was unchanged after the PR base: The PR base is an ancestor of current main, and no package-publish workflow or its contract-test changes appeared between that base and current main.
Patch preserves the execution path: The submitted diff adds log-only escaping while retaining the shell-quoted rerunnable script, and adds a focused workflow contract test; the branch changes 12/1 workflow lines and adds 19 test lines.
Findings None None.
Security None None.

How this fits together

ClawHub’s reusable package-publish workflow turns caller inputs into CLI arguments, writes a rerunnable shell script, and logs the resolved command. GitHub Actions consumes that log while the package CLI consumes the original argument list.

flowchart LR
  Caller[Workflow caller metadata] --> Resolve[Resolve publish command]
  Resolve --> Arguments[Package CLI arguments]
  Arguments --> Script[Runnable shell script]
  Arguments --> Log[Resolved command log]
  Log --> Runner[GitHub Actions runner]
  Arguments --> Publish[Package publish CLI]
Loading

Before merge

  • Resolve merge risk (P1) - Required checks are presently non-discriminating because the supplied context reports a base-wide audit failure; normal merge gating needs a clean-base rerun or resolution of chore: restore a clean bun audit so CI stops failing on every branch #3446.
  • Complete next step (P2) - No mechanical repair remains; a maintainer can merge after normal checks become discriminating again.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test delta workflow +12/-1, tests +19/-0 The small production change is paired with focused coverage of the log-versus-script boundary.

Merge-risk options

Maintainer options:

  1. Decide the mitigation before merge
    Land the log-only escaping with its regression contract after normal merge-gate validation, keeping publisher arguments and the generated rerunnable script unchanged.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Technical review

Best possible solution:

Land the log-only escaping with its regression contract after normal merge-gate validation, keeping publisher arguments and the generated rerunnable script unchanged.

Do we have a high-confidence way to reproduce the issue?

Yes. The supplied real reusable-workflow dispatch reproduces an injected annotation from newline-bearing metadata on the PR base, and current main retains the same affected workflow lines.

Is this the best way to solve the issue?

Yes. Escaping only the diagnostic rendering prevents runner parsing without changing the CLI argument list or the rerunnable shell script; rejecting or rewriting metadata at execution time would be broader and less compatible.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against d9157142e9c5.

Labels

Label justifications:

  • P2: This is a bounded security hardening fix for reusable publishing workflows, with no evidence of an active broader compromise.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (linked_artifact): The PR supplies a linked live reusable-workflow before/after run at pinned baseline and branch SHAs, including the disappearance of the injected check annotation.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR supplies a linked live reusable-workflow before/after run at pinned baseline and branch SHAs, including the disappearance of the injected check annotation.

Evidence

What I checked:

  • Current main still exposes the behavior: Current main accepts caller-controlled changelog, categories, and topics values into the command list, then prints a shell-quoted command line; shell quoting preserves embedded newlines in stdout. (.github/workflows/package-publish.yml:575, d9157142e9c5)
  • The affected workflow was unchanged after the PR base: The PR base is an ancestor of current main, and no package-publish workflow or its contract-test changes appeared between that base and current main. (.github/workflows/package-publish.yml:575, d9157142e9c5)
  • Patch preserves the execution path: The submitted diff adds log-only escaping while retaining the shell-quoted rerunnable script, and adds a focused workflow contract test; the branch changes 12/1 workflow lines and adds 19 test lines. (.github/workflows/package-publish.yml:581, 25aec92957aa)
  • Live before-and-after proof: The PR body links one reusable-workflow dispatch with identical newline-bearing metadata: the baseline records an injected notice annotation, while the branch head records none and prints an escaped one-line payload. (25aec92957aa)
  • Feature history: The caller metadata path appears to date to the package catalog-metadata change, which touched both the workflow and its contract test. (.github/workflows/package-publish.yml:508, 6d935f05957b)

Likely related people:

  • Sergio Peschiera: Commit 6d935f0 introduced the package catalog-metadata workflow change and touched the corresponding contract test. (role: introduced caller metadata path; confidence: high; commits: 6d935f05957b; files: .github/workflows/package-publish.yml, src/__tests__/package-publish-workflow.test.ts)
  • Patrick Erichsen: Commit 87ca030 is the preceding recorded change to the reusable package-publish workflow before the metadata addition. (role: prior workflow contributor; confidence: medium; commits: 87ca030c30f3; files: .github/workflows/package-publish.yml)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Revalidate required checks after the base-wide audit failure is resolved or a clean-base rerun is available.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (7 earlier review cycles)
  • reviewed 2026-08-10T16:38:33.128Z sha 25aec92 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-10T21:06:43.796Z sha 25aec92 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-10T23:07:54.930Z sha 25aec92 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-11T03:09:23.811Z sha 25aec92 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-11T06:09:53.581Z sha 25aec92 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-11T13:11:06.833Z sha 25aec92 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-11T13:16:10.314Z sha 25aec92 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. labels Aug 10, 2026
@Patrick-Erichsen
Patrick-Erichsen merged commit 8bf424c into openclaw:main Aug 11, 2026
98 of 108 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants