Detect and replace heredocs in generated workflow YAML - #53183
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
✅ Ponytail Reviewer completed successfully! pr-diff.patch is empty (0 bytes) — no diff content available to review for over-engineering. Skipping Ponytail review.
|
|
✅ Test Quality Sentinel completed test quality analysis. Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch PR file list
|
|
✅ Design Decision Gate 🏗️ completed the design decision gate check.
|
|
✅ PR Code Quality Reviewer completed the code quality review.
|
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅
|
Comment MemoryNote This comment is managed by comment memory.It stores persistent context for this thread in the code block at the top of this comment. Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
|
There was a problem hiding this comment.
Request changes
This migration is headed in the right direction, but it still leaves an unfixed heredoc write in the early comment-memory bootstrap path, so the new enforcement is incomplete and the attack surface you described is still present in generated workflow shell.
Blocking themes
compiler_yaml_runtime_setup.gostill writesconfig.jsonvia a shell heredoc for comment-memory bootstrap.- The new analyzer only prevents new occurrences and explicitly suppresses existing ones, so that remaining path is not theoretical debt — it is still live generated shell.
- Until that bootstrap write is migrated to the same JS file renderer approach, the PR does not fully deliver the promised hardening.
🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 22 AIC · ⌖ 6.3 AIC · ⊞ 6.9K
Comment /review to run again
Documents the decision to introduce a Go linter (generatedyamlheredoc) and JavaScript file renderer (create_files.cjs) as a replacement for shell-evaluated heredocs in generated workflow YAML. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🏗️ Design Decision Gate — ADR RequiredThis PR makes significant changes to core business logic (206 new lines in business-logic directories) but did not have a linked Architecture Decision Record (ADR). 📄 Draft ADR committed:
📋 What to do next
Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision. ❓ Why ADRs Matter
ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you. 📋 Michael Nygard ADR Format ReferenceAn ADR must contain these four sections to be considered complete:
All ADRs are stored in
|
There was a problem hiding this comment.
This PR makes solid progress on a real security concern — heredocs in generated YAML expose content to shell evaluation, and the JavaScript renderer (create_files.cjs) is the right long-term direction. The linter + nolint tracking is a good incremental approach.
Five issues flagged inline, two of which are blocking:
| Severity | File | Issue |
|---|---|---|
| 🔴 Blocking | create_files.cjs:72 |
O_NOFOLLOW || 0 silently disables symlink protection when flag is unavailable |
| 🟡 Non-blocking | create_files.cjs:103 |
TOCTOU gap between realpathSync and writeFile for parent directory symlinks |
| 🟡 Non-blocking | mcp_setup_safe_outputs.go:46 |
GH_AW_FILE_ROOT emitted unquoted via %s — inconsistent with writeYAMLEnv pattern |
| 🟡 Non-blocking | generatedyamlheredoc.go:91 |
Linter only catches cat <<; tee <<, node <<, read << are undetected |
| 🟡 Non-blocking | create_files.test.cjs:57 |
No test for symlinked-parent-directory escape path |
The blocking item is the O_NOFOLLOW || 0 silent degradation — if the flag is undefined on a platform, the symlink guard is silently removed, which undermines the primary security guarantee of this new renderer.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
proxy.golang.org
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "proxy.golang.org"See Network Configuration for more information.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 107.3 AIC · ⌖ 8.71 AIC · ⊞ 5.6K
| */ | ||
| function writeFile(filePath, content) { | ||
| const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0); | ||
| const fd = fs.openSync(filePath, flags, 0o600); |
There was a problem hiding this comment.
Security: O_NOFOLLOW silently degrades when unavailable
The expression (fs.constants.O_NOFOLLOW || 0) is a silent fallback — if O_NOFOLLOW is undefined (e.g., on certain platforms), the flag is dropped and the symlink guard disappears without warning. Since this module's core purpose is to prevent path-escape attacks, a missing flag should be a hard failure:
const O_NOFOLLOW = fs.constants.O_NOFOLLOW;
if (!O_NOFOLLOW) {
throw new Error(`${ERR_SYSTEM}: O_NOFOLLOW flag is not available on this platform; cannot write files safely`);
}
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | O_NOFOLLOW;This makes the protection explicit and non-optional.
@copilot please address this.
| makeDirectory(root); | ||
| const resolvedRoot = fs.realpathSync(root); | ||
|
|
||
| for (const directory of config.directories || []) { |
There was a problem hiding this comment.
Potential TOCTOU between assertPathWithin and writeFile
The code calls makeDirectory(parentPath), then assertPathWithin(resolvedRoot, fs.realpathSync(parentPath)), and then writeFile(filePath, ...). Between the realpathSync check and the openSync, a symlink could be swapped in at parentPath (race window).
O_NOFOLLOW on the final file open protects against the file being a symlink, but not against the parent directory being replaced by a symlink after the realpathSync check. To close this gap, the assertPathWithin check on the parent should happen before makeDirectory, not after:
const filePath = resolveRelativePath(resolvedRoot, item.path);
const parentPath = path.dirname(filePath);
// Assert path is safe before creating anything on disk.
const expectedParent = path.resolve(resolvedRoot, path.dirname(item.path));
assertPathWithin(resolvedRoot, expectedParent);
makeDirectory(parentPath);
// Now recheck after creation in case a symlink was raced in:
assertPathWithin(resolvedRoot, fs.realpathSync(parentPath));
writeFile(filePath, env[item.content_env] || "");@copilot please address this.
| yaml.WriteString(" - name: Generate Safe Outputs Config\n") | ||
| fmt.Fprintf(yaml, " uses: %s\n", getCachedActionPin("actions/github-script", workflowData)) | ||
| yaml.WriteString(" env:\n") | ||
| fmt.Fprintf(yaml, " GH_AW_FILE_ROOT: %s\n", constants.GhAwRootDir) |
There was a problem hiding this comment.
Inconsistent YAML quoting for GH_AW_FILE_ROOT
This line emits GH_AW_FILE_ROOT unquoted using %s, while all other env vars in this block go through writeYAMLEnv which uses %q (double-quoted YAML scalar). The current constant value (${{ runner.temp }}/gh-aw) is safe unquoted, but using an inconsistent pattern is fragile if the constant ever changes.
Prefer the existing helper for consistency:
writeYAMLEnv(yaml, " ", "GH_AW_FILE_ROOT", constants.GhAwRootDir)Note: %q wraps the value in "..." which is valid YAML and does not interfere with GitHub Actions expression evaluation.
@copilot please address this.
| beforeIsWord := catIndex > 0 && isShellWordByte(line[catIndex-1]) | ||
| afterIndex := catIndex + len("cat") | ||
| afterIsWord := afterIndex < len(line) && isShellWordByte(line[afterIndex]) | ||
| if beforeIsWord || afterIsWord { |
There was a problem hiding this comment.
Linter misses tee <<, append (>><<), and process-substitution heredocs
The lineContainsShellHeredoc function only recognises the cat command. Other patterns that embed content via heredocs in generated YAML are not caught:
tee << 'EOF'(common alternative tocat)node script.js << 'EOF'(piping stdin via heredoc to a process)read VAR << 'EOF'
If any of these patterns appear in future generated YAML they will pass undetected. Consider generalising the detection to match << (the heredoc operator) after any shell word, not only after cat:
func lineContainsShellHeredoc(line string) bool {
_, after, found := strings.Cut(line, "<<")
if !found {
return false
}
// Skip <<< (here-string) and <<EOF without space (bit-shift)
return !strings.HasPrefix(after, "<") && len(after) > 0
}@copilot please address this.
| fs.writeFileSync(filePath, "old", { mode: 0o666 }); | ||
| fs.chmodSync(filePath, 0o666); | ||
|
|
||
| renderFiles({ files: [{ path: "config.json", content_env: "CONTENT" }] }, { CONTENT: "new" }, root); |
There was a problem hiding this comment.
Missing test: symlink escape via symlinked parent directory
The test suite checks traversal (../secret) and missing env vars, but there is no test for the case where a symlink directory (rather than a symlink file) is placed inside the root after makeDirectory. The O_NOFOLLOW flag only protects against the final file target being a symlink — it does not protect against a parent directory being replaced by a symlink between the realpathSync check and the openSync call.
A test that:
- Creates the root
- Creates an intermediate dir that is a symlink pointing outside the root
- Calls
renderFileswith a path that traverses through the symlink
...would verify the defence is effective (or expose the TOCTOU gap noted in the adjacent inline comment).
@copilot please address this.
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes on correctness and coverage gaps.
📋 Key Themes & Highlights
Key Themes
- Silent security degradation:
O_NOFOLLOW || 0drops the symlink guard on platforms without that constant, with no error or warning. - Linter coverage gap:
lineContainsShellHeredocdetects onlycat <<; other heredoc forms (tee <<,node <<) pass undetected — the linter name implies broader coverage than it provides. - Hardcoded JSON schema: the
GH_AW_FILE_CONFIGvalue is a raw Go string literal that will drift if theFileRenderConfigschema evolves. - 19
(nolint/redacted)suppression sites: well-documented as migration debt — the tracking approach is sound. The "early config" suppression (compiler_yaml_runtime_setup.go) has a clear architectural reason.
Positive Highlights
- ✅ Layered path-traversal and symlink defences in
create_files.cjs(resolveRelativePath+realpathSyncpost-mkdir) are thorough. - ✅ Go linter test data covers all intended patterns (herestring
<<<, arithmetic shifts, word-boundary false positives) cleanly. - ✅ Replacing the heredoc with an
actions/github-scriptstep that reads content from env vars is architecturally correct — no base64 encoding, no shell evaluation. - ✅ Test suite for
create_files.cjscovers the most important security property (shell injection canary) and permission restriction. - ✅
writeYAMLEnvusing%qfor the sanitized config value is a good defence against YAML structure injection.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 129.2 AIC · ⌖ 9.14 AIC · ⊞ 7.7K
Comment /matt to run again
| * @param {string} content | ||
| */ | ||
| function writeFile(filePath, content) { | ||
| const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0); |
There was a problem hiding this comment.
[/diagnosing-bugs] O_NOFOLLOW || 0 silently drops the symlink guard on platforms where fs.constants.O_NOFOLLOW is undefined (e.g. Windows). A successful openSync with flags 0 provides no symlink protection; the security property evaporates without any error.
💡 Suggestion
Assert the flag is available rather than silently zeroing it:
const O_NOFOLLOW = fs.constants.O_NOFOLLOW;
if (O_NOFOLLOW === undefined) {
throw new Error(`${ERR_SYSTEM}: O_NOFOLLOW is not available on this platform`);
}
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | O_NOFOLLOW;If Windows runners are explicitly out-of-scope, a one-line comment stating that assumption prevents future readers from softening the guard further.
@copilot please address this.
| return false | ||
| } | ||
|
|
||
| func lineContainsShellHeredoc(line string) bool { |
There was a problem hiding this comment.
[/codebase-design] lineContainsShellHeredoc detects only cat << patterns. Other valid heredoc forms — tee <<, node renderer.cjs <<, bare bash -c ... << — would pass undetected. The linter is named generatedyamlheredoc (not catamlheredoc), so callers expect broader coverage.
💡 Suggestion
Either narrow the name/doc to cat-only and make that explicit in the diagnostic message, or extend detection to any <word> << pattern (anything that is a non-empty token immediately before << that is not itself <<):
// lineContainsShellHeredoc returns true for any shell heredoc of the form:
// <cmd> << or << (standalone redirect with no preceding word)
func lineContainsShellHeredoc(line string) bool {
idx := strings.Index(line, "<<")
for idx >= 0 {
after := line[idx+2:]
if strings.HasPrefix(after, "<") { // skip here-string <<<
line = after
idx = strings.Index(line, "<<")
continue
}
// Anything before << that is a non-space token is a heredoc command
pre := strings.TrimRight(line[:idx], " \t")
if pre != "" {
return true
}
line = after
idx = strings.Index(line, "<<")
}
return false
}This also removes the special cat-word-boundary logic, simplifying the function.
@copilot please address this.
| const parentPath = path.dirname(filePath); | ||
| makeDirectory(parentPath); | ||
| assertPathWithin(resolvedRoot, fs.realpathSync(parentPath)); | ||
| writeFile(filePath, env[item.content_env] || ""); |
There was a problem hiding this comment.
[/diagnosing-bugs] env[item.content_env] || "" conflates two distinct cases: (a) the env var is set to an empty string (valid, write empty file) and (b) the env var is undefined (already caught on line 116). Because the hasOwnProperty check on line 116 already guards against missing keys, the || "" here is unreachable for undefined, but it will silently convert false, 0, or other falsy values passed through process.env to "" rather than their string form.
💡 Suggestion
process.env values are always strings or undefined, so || "" is safe in practice — but it obscures that invariant. Prefer the explicit cast:
writeFile(filePath, String(env[item.content_env] ?? ""));or simply:
writeFile(filePath, env[item.content_env]);since by this point the key is guaranteed to exist. A brief comment noting the invariant would help future readers.
@copilot please address this.
| continue | ||
| } | ||
| value, err := strconv.Unquote(lit.Value) | ||
| if err != nil || !containsShellHeredoc(value) { |
There was a problem hiding this comment.
[/tdd] The linter skips files matching filecheck.ShouldSkipFilename, which excludes Go test files (_test.go). But the primary production callsites being guarded are inside pkg/workflow/*.go — none of which are test files. The existing Go analysis test covers the testdata fixture well.
However, there is no regression test asserting that a new cat << inserted into pkg/workflow/ (without a (nolint/redacted)) is actually caught and the CI lint step fails. The 19 suppression sites are tracked correctly, but the linter's effectiveness against future regressions (someone accidentally adding a heredoc in a new file) is not directly exercised at the integration level.
💡 Suggestion
Add a note in the linter's README.md entry explaining how to verify the linter fires (e.g. gh aw lint or the relevant make lint target), so that reviewers of future heredoc-adjacent PRs know how to confirm the guard is active.
@copilot please address this.
| fmt.Fprintf(yaml, " uses: %s\n", getCachedActionPin("actions/github-script", workflowData)) | ||
| yaml.WriteString(" env:\n") | ||
| fmt.Fprintf(yaml, " GH_AW_FILE_ROOT: %s\n", constants.GhAwRootDir) | ||
| writeYAMLEnv(yaml, " ", "GH_AW_FILE_CONFIG", `{"files":[{"path":"safeoutputs/config.json","content_env":"GH_AW_SAFE_OUTPUTS_CONFIG"}]}`) |
There was a problem hiding this comment.
[/codebase-design] The GH_AW_FILE_CONFIG value is a hardcoded JSON string literal inside Go source. If the FileRenderConfig schema grows (e.g. to support file modes, symlinks, or encoding options), this literal will need updating in two separate places: the Go string here and the JavaScript parseConfig schema.
💡 Suggestion
Consider marshalling a Go struct matching FileRenderConfig rather than maintaining a raw JSON string:
type fileRenderItem struct {
Path string `json:"path"`
ContentEnv string `json:"content_env"`
}
type fileRenderConfig struct {
Files []fileRenderItem `json:"files"`
}
config, _ := json.Marshal(fileRenderConfig{
Files: []fileRenderItem{{Path: "safeoutputs/config.json", ContentEnv: "GH_AW_SAFE_OUTPUTS_CONFIG"}},
})
writeYAMLEnv(yaml, " ", "GH_AW_FILE_CONFIG", string(config))This keeps the schema DRY and makes compile-time typos impossible.
@copilot please address this.
|
@copilot this PR is ready for the next finishing pass. Please address these items, newest first:
Please refresh the branch if needed, rerun the relevant validation, and run the
|
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
|
@copilot resolve the merge conflicts on this branch. |
|
@copilot this PR is ready for the next finishing pass. Please refresh the branch if needed, run the
|
|
✅ Smoke OTEL completed successfully!
|
|
✅ Smoke Copilot Small completed successfully!
|
|
📰 BREAKING: Smoke Copilot is now investigating this pull request. Sources say the story is developing... |
|
📰 BREAKING: Smoke Copilot - AOAI (apikey) is now investigating this pull request. Sources say the story is developing... |
|
Heredocs hid shell risk, Inspired by this PR's shift from shell heredocs to a safe JavaScript file renderer.
|
|
Smoke test summary:
|
Agent Container Tool Check
Result: 9/12 tools available ❌ FAIL
|
Smoke test FAILPR reads: Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
|
|
Commit pushed:
|
Copilot Engine Smoke Test — Run 31974721397PR: Detect and replace heredocs in generated workflow YAML
Overall status: FAIL (1 test failed: Serena/Go language server) PR author: Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
|
Comment MemoryNote This comment is managed by comment memory.It stores persistent context for this thread in the code block at the top of this comment. Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
|
There was a problem hiding this comment.
Smoke test automated review - all good.
Warning
Firewall blocked 6 domains
The following domains were blocked by the firewall during workflow execution:
accounts.google.comandroid.clients.google.comclients2.google.comcontentautofill.googleapis.comwww.google.comwww.gstatic.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
📰 BREAKING: Report filed by Smoke Copilot · auto · 38.4 AIC · ⌖ 2.35 AIC · ⊞ 8.9K
Comment /smoke-copilot to run again
Add label smoke to run again
Comments that could not be inline-anchored
.github/workflows/ab-testing-advisor.lock.yml:17
Smoke test inline comment #1 - lock file regenerated as part of PR.
Comment MemoryNote This comment is managed by comment memory.It stores persistent context for this thread in the code block at the top of this comment.
|
💥 Smoke Test Results — Run 31974687259Core Tests #1–#12: PR Review Tests #13–#19: Overall Status: PARTIAL Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
|
There was a problem hiding this comment.
💥 Automated smoke test review - all systems nominal! — Run 31974687259
Warning
Firewall blocked 6 domains
The following domains were blocked by the firewall during workflow execution:
accounts.google.comandroid.clients.google.comclients2.google.comcontentautofill.googleapis.comwww.google.comwww.gstatic.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
💥 [THE END] — Illustrated by Smoke Claude · sonnet46 · 80.4 AIC · ⌖ 17.9 AIC · ⊞ 6.6K
Comment /smoke-claude to run again
| * @param {string} value | ||
| * @returns {FileRenderConfig} | ||
| */ | ||
| function parseConfig(value) { |
There was a problem hiding this comment.
👍 The parseConfig function cleanly validates the files array presence. Consider also validating that each FileRenderItem has non-empty path and content_env fields at parse time to surface config errors earlier.
| @@ -0,0 +1,114 @@ | |||
| // Package generatedyamlheredoc implements a Go analysis linter that flags | |||
There was a problem hiding this comment.
🔍 Good addition of the generatedyamlheredoc analyzer! This will help enforce the security boundary around shell injection in generated YAML heredocs. Adding a brief package-level comment explaining the threat model would help future maintainers.
|
Smoke Test Results: Overall: FAIL
Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
|
Comment MemoryNote This comment is managed by comment memory.It stores persistent context for this thread in the code block at the top of this comment. Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
|
There was a problem hiding this comment.
Warning
Firewall blocked 6 domains
The following domains were blocked by the firewall during workflow execution:
accounts.google.comandroid.clients.google.comclients2.google.comcontentautofill.googleapis.comwww.google.comwww.gstatic.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
📰 BREAKING: Report filed by Smoke Copilot - AOAI (apikey) · o40mini · 50.2 AIC · ⌖ 2.59 AIC · ⊞ 19.2K
Comment /smoke-copilot-aoai-apikey to run again
Add label smoke to run again
|
🎉 This pull request is included in a new release. Release: |
Generated YAML heredocs expose workflow content to shell injection. This adds enforcement against new heredocs and begins migrating file generation to JavaScript.
Changes
Static enforcement
generatedyamlheredoc, a Go analyzer detecting heredocs embedded in generated workflow shell.Safe file rendering
create_files.cjsfor writing environment-provided content without shell evaluation or base64 encoding.0600permissions.Initial migration
GH_AW_SAFE_OUTPUTS_CONFIG.✨ PR Review Safe Output Test - Run 31974687259> [!WARNING]