Summary
The safe-outputs sanitizer unconditionally rewrites Slack mrkdwn links of the form <url|label> in the string inputs of custom safe-output jobs (safe-outputs.jobs.*). convertXmlTags in sanitize_content_core.cjs treats the link as an unknown XML tag and converts the angle brackets to parentheses, which breaks the link once it reaches Slack. There is currently no way to opt out per input.
Reproduction
-
Define a custom safe-output job that posts to Slack (e.g. wrapping slackapi/slack-github-action with chat.postMessage), with a text: { type: string } input.
-
Have the agent call the generated safe-output tool with valid Slack mrkdwn:
Tracking issue: <https://github.com/octo-org/octo-repo/issues/123|Build failure — Build #456>
-
After the "Ingest agent output" step, agent_output.json contains:
Tracking issue: (https://github.com/octo-org/octo-repo/issues/123|Build failure — Build #456)
-
When that text is posted, Slack auto-linkifies the now-bare URL. The auto-detected URL runs to the first whitespace, so it swallows |Build, and Slack percent-encodes the pipe (its own link delimiter). The message renders as a broken link:
Tracking issue: (https://github.com/octo-org/octo-repo/issues/123%7CBuild failure — Build #456)
Note the domain here is github.com, which is on the default allowed-domains list — this is not URL redaction. Step 3 is reproducible by calling sanitizeContent from gh-aw-actions directly on the string in step 2 (verified at github/gh-aw-actions@5c2fe86 / v0.79.6, and the relevant code is unchanged on current main).
Root cause
collect_ndjson_output.cjs → validateFieldWithInputSchema runs sanitizeContent() on every string input of a custom safe-output job.
sanitizeContentCore → convertXmlTags rewrites any <...> whose "tag name" is not on the allowed HTML tag list to (...). A Slack link <https://…|label> parses as tag name https, so it is rewritten.
The sanitization is understandable as prompt-injection/XSS defense for content destined for GitHub issues/PRs, but custom safe-output jobs deliver content to arbitrary sinks with their own syntaxes (Slack mrkdwn being a common one), where this rewrite is destructive.
Related but distinct: #18829 covers <...> inside code regions; this report is about plain-text content of custom safe-job inputs.
Suggested fixes (any of these would work)
- Make
convertXmlTags autolink-aware: preserve <https://…> and <https://…|label> spans (angle-bracket autolinks are also valid CommonMark/GFM, so today's behavior mangles legitimate markdown autolinks too). The inner URL would still be subject to the existing protocol/domain filtering.
- Per-input opt-out on custom safe-job input schemas, e.g.
sanitize: false or a format: slack-mrkdwn hint, so component authors can accept the trade-off explicitly for non-GitHub sinks.
- At minimum, document the limitation so component authors know to convert markdown links to mrkdwn after ingestion, inside the safe-output job itself.
Environment
gh-aw compiler v0.79.6 (lock file compiler_version: v0.79.6; runtime scripts from github/gh-aw-actions v0.79.6, SHA 5c2fe865)
- Custom safe-output job under
safe-outputs.jobs.* posting via slackapi/slack-github-action@v3.0.1
Summary
The safe-outputs sanitizer unconditionally rewrites Slack mrkdwn links of the form
<url|label>in the string inputs of custom safe-output jobs (safe-outputs.jobs.*).convertXmlTagsinsanitize_content_core.cjstreats the link as an unknown XML tag and converts the angle brackets to parentheses, which breaks the link once it reaches Slack. There is currently no way to opt out per input.Reproduction
Define a custom safe-output job that posts to Slack (e.g. wrapping
slackapi/slack-github-actionwithchat.postMessage), with atext: { type: string }input.Have the agent call the generated safe-output tool with valid Slack mrkdwn:
After the "Ingest agent output" step,
agent_output.jsoncontains:When that text is posted, Slack auto-linkifies the now-bare URL. The auto-detected URL runs to the first whitespace, so it swallows
|Build, and Slack percent-encodes the pipe (its own link delimiter). The message renders as a broken link:Note the domain here is
github.com, which is on the default allowed-domains list — this is not URL redaction. Step 3 is reproducible by callingsanitizeContentfromgh-aw-actionsdirectly on the string in step 2 (verified atgithub/gh-aw-actions@5c2fe86/ v0.79.6, and the relevant code is unchanged on currentmain).Root cause
collect_ndjson_output.cjs→validateFieldWithInputSchemarunssanitizeContent()on every string input of a custom safe-output job.sanitizeContentCore→convertXmlTagsrewrites any<...>whose "tag name" is not on the allowed HTML tag list to(...). A Slack link<https://…|label>parses as tag namehttps, so it is rewritten.The sanitization is understandable as prompt-injection/XSS defense for content destined for GitHub issues/PRs, but custom safe-output jobs deliver content to arbitrary sinks with their own syntaxes (Slack mrkdwn being a common one), where this rewrite is destructive.
Related but distinct: #18829 covers
<...>inside code regions; this report is about plain-text content of custom safe-job inputs.Suggested fixes (any of these would work)
convertXmlTagsautolink-aware: preserve<https://…>and<https://…|label>spans (angle-bracket autolinks are also valid CommonMark/GFM, so today's behavior mangles legitimate markdown autolinks too). The inner URL would still be subject to the existing protocol/domain filtering.sanitize: falseor aformat: slack-mrkdwnhint, so component authors can accept the trade-off explicitly for non-GitHub sinks.Environment
gh-awcompiler v0.79.6 (lock filecompiler_version: v0.79.6; runtime scripts fromgithub/gh-aw-actionsv0.79.6, SHA5c2fe865)safe-outputs.jobs.*posting viaslackapi/slack-github-action@v3.0.1