Skip to content

feat(nodes): add Bash node for running shell commands#158

Draft
xujustinj wants to merge 1 commit into
mainfrom
feat/bash-node
Draft

feat(nodes): add Bash node for running shell commands#158
xujustinj wants to merge 1 commit into
mainfrom
feat/bash-node

Conversation

@xujustinj

Copy link
Copy Markdown
Contributor

Closes #157.

Implements the idea from #157: every shell command can be viewed as a workflow node with arguments in and its standard streams as output files.

What's new

BashNode (type: "Bash"):

  • command param — the shell command, rendered as a Jinja template so values from upstream nodes substitute in via {{ name }} (StrictUndefined, so a missing variable errors instead of blanking).
  • combine_output param — when true, merges stderr into stdout (2>&1) and switches the node's output shape to a single combined output file (via dynamic_output_type), as described in the issue.
  • arguments input — a StringMapValue[StringValue] (defaults empty, so argument-free commands need no edge) supplying the Jinja variables.
  • Outputsstdout + stderr files and an exit_code, or a single output file + exit_code when combining.
  • Runs in the user's $SHELL (falls back to /bin/bash) in the engine's own environment, matching the maintainer discussion on the issue.

Security

This is effectively remote code execution — it runs arbitrary commands in the engine's environment. The node/module descriptions and docs flag it as trusted, locally-operated engines only; never expose it on an engine that runs untrusted workflows or accepts workflows over the network.

Notes / open questions

  • Non-zero exits don't raiseexit_code is just an output, leaving failure handling to downstream nodes. Happy to add a check param that raises on non-zero if preferred.
  • .bashrc sourcing — the issue mused about full interactive-shell init. This uses a plain non-interactive bash -c (no -i/-l) to avoid hangs and job-control noise; users add init via && as the issue suggests.

Testing

New tests/test_shell.py (4 tests): basic command, Jinja arg substitution through an input edge, non-zero exit + stderr capture, combined output. Full suite green: ruff (check + format), pyright (0 errors), 648 passed.

🤖 Generated with Claude Code

Implements #157. Every shell command can be viewed as a workflow node:
a Jinja-rendered command line runs in a subprocess and its standard
streams become output files.

- `command` param is a Jinja template rendered with the node's
  `arguments` input, so upstream values substitute in via `{{ name }}`.
- `combine_output` param merges stderr into stdout (2>&1), switching the
  output shape to a single combined file (via dynamic_output_type).
- Outputs `stdout`/`stderr` files plus an `exit_code`.
- Runs in the user's $SHELL (falls back to /bin/bash) in the engine's
  own environment, per the maintainer discussion on the issue.

This is effectively RCE and is documented as trusted-local-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Arbitrary script execution via bash

1 participant