shellflow is a shell-native, agentless automation tool that runs commands over ssh - concurrently, idempotently and 10x faster than ansible.
A deploy run is a plain Bash script annotated with # @ comment
directives. shellflow interprets the directives and drives the blocks
locally, across hosts, and through file copies — wrapping the system
ssh/rsync/scp/bash. No Python, no YAML, no agent, no SSH library, no
target-side daemon.
#!/usr/bin/env shellflow
# @server trade trade
# @server api api
# @group all trade,api
# @local
VERSION=$(git rev-parse --short HEAD)
# @export VERSION
# @copy target/release/myapp-$VERSION -> /tmp/shellflow/myapp-$VERSION @all
# @remote all
# @only_if test -f /etc/shellflow/myapp
sudo systemctl restart myappBecause directives are comments, the file is 100% valid Bash — bash deploy.sh runs it unchanged, and your editor highlights it perfectly.
- Agentless & shell-native — zero install on targets: they only need
bash.shellflowconnects with your systemssh, so~/.ssh/config, keys,ssh-agent, jump hosts, and YubiKeys just work. - Mina-style streaming — each remote block is streamed to every host over a
single
ssh host bash -sconnection from memory. No per-command round trips. - Deployer-style pipeline —
@localbuild →@copy→@remotedeploy, with concurrent multi-host fan-out (JoinSet+ semaphore,--paralleldefaults to all hosts). - Detailed debugging —
-v/-vv/-vvv:-v: plan preview + per-step timing.-vv: exactssh/rsync/scpargv and full payload previews.-vvv: injectsset -xinto every payload andssh -von the wire, so you watch Bash trace lines live (stderr shown yellow, per-host prefixed).
- Idempotent dry-run & diff —
--dry-run/--diffsyntax-check payloads (bash -n, remotebash -n -s) and itemize file changes viarsync --dry-run -i. Nothing is written;--diffhides status prose and shows only the change list. - State passing —
@export VARcaptures a variable from a local block and injects it into later@copypaths,@remoteblocks, and@only_ifguards as$VAR/${VAR}/ env. - Graceful file copies —
@copyuses rsync when the target has it (with amkdir -premote wrapper), and automatically falls back tossh mkdir -p+scpon hosts without rsync. Destination directories are always created. - Guards & timeouts —
@only_if <cmd>skips a block where the precondition fails;--timeout SECS/@timeout SECSkills hung hosts (guards included). - Lifecycle safety — Ctrl-C/SIGTERM terminates in-flight children (no
orphans), prints a summary of completed steps, and exits 130; a preflight
check fails fast if
bash/ssh/rsync/scpare missing. - Output modes —
--output groupedprints each host's logs as one block;--log-file PATHappends every streamed line (tagged host+stream) for audit. - Secrets — literal
@env KEY=VALUEvalues are masked (***) in previews, trace output, and log files, and never appear in any argv. The@env KEYpassthrough form inherits from shellflow's own environment and is not redacted.
cargo install --path bin/shellflow
# or build from the workspace
cargo build --release -p shellflowRequires bash on the controller and targets, plus ssh/rsync/scp on the
controller (system tools — your ~/.ssh/config, keys, agent, and jump hosts
just work). Targets only need bash.
A directive is a comment whose first non-whitespace token is # @. All other
lines belong to the current block.
| Directive | Syntax | Semantics |
|---|---|---|
@server |
# @server <name> <ssh-spec> |
Alias a host. <ssh-spec> = [user@]host[:port], or a ~/.ssh/config host alias |
@group |
# @group <name> <member>[,<member>…] |
Alias a group of servers |
@env |
# @env <KEY> / # @env <KEY>=<value> |
Inject an env var into later blocks; no =value copies from shellflow's environment. Literal values are masked in output |
@local |
# @local |
Following lines run locally (default) |
@remote |
# @remote <target> |
Following lines stream to the target (alias, group, or raw spec) |
@copy |
# @copy <src> -> <dst> @<target> [--delete] |
Copy a local path to the target (rsync, or scp fallback); creates the destination directory; supports $VAR interpolation |
@export |
# @export <VAR>[,<VAR>…] |
Capture variables from the preceding local block into run state |
@timeout |
# @timeout <seconds> |
Per-step timeout for the next block |
@name |
# @name <label> |
Name the next block (for --only/--skip and reporting) |
@only_if |
# @only_if <command> |
Skip the next block where <command> fails |
Key rules:
- Every directive flushes the pending block; blocks with no executable statement are dropped.
- Local and remote blocks run with
set -euinjected (set -euxat-vvv), so a failing command stops the block. @copypaths are parsed by shellflow, not Bash — use@exportto capture values and reference them as$VAR. An unresolved variable is a hard error in a real run (and a warning + skip in dry-run/check).@copypaths must not contain the literal->; the target is the last whitespace-separated token; paths with spaces are unsupported.@only_ifguards run on each host with the same env as the block.- Lines inside heredocs are not interpreted as directives.
USAGE: shellflow [OPTIONS] [SCRIPT]
ARGS:
<SCRIPT> Deploy script path [default: deploy.sh]
OPTIONS:
-v, --verbose... -v info, -vv show commands + payloads,
-vvv inject set -x tracing and ssh -v
-n, --dry-run Simulate; no writes. Syntax-checks payloads.
-d, --diff Show itemized file changes; implies no writes.
-t, --target <TARGET> Restrict to these servers/groups (comma-separated)
-o, --only <STEP> Run only matching blocks (by name or 1-based index)
-s, --skip <STEP> Skip matching blocks (repeatable)
-p, --parallel <N> Max concurrent hosts per step [default: all]
-c, --continue-on-error Continue after a failed host/step; print summary
-k, --check Syntax-check only: local bash -n, remote bash -n -s
--timeout <SECS> Per-step timeout for all steps
--output <MODE> stream (default) | grouped
-l, --log-file <PATH> Append streamed lines (tagged host+stream)
--no-color Disable ANSI colors
-h, --help Print help
-V, --version Print version
Exit codes: 0 success · 1 plan/parse error · 2 CLI usage · 3
transport/setup failure · 4 script execution failure · 130 interrupted.
shellflow playbooks/deploy.sh # real run against trade + api
shellflow -vv playbooks/deploy.sh # show every command + payload
shellflow -vvv playbooks/deploy.sh # set -x + ssh -v live trace
shellflow --dry-run --diff playbooks/deploy.sh # preview changes, change nothing
shellflow -t api playbooks/deploy.sh # deploy to a single host
shellflow --only facts playbooks/deploy.sh # run just one named block
shellflow -k playbooks/deploy.sh # syntax-check everything
shellflow -c --only ship-marker playbooks/deploy.sh # copy step, keep goingplaybooks/deploy.sh is a runnable playbook that
exercises every DSL feature against real ~/.ssh/config hosts (trade and
api) — read-only system facts and /tmp-confined changes, with parallel
fan-out, guards, timeouts, env passthrough, and secret masking. See
docs/design.md for the full design.
just setup # install dev tools (cargo-mutants, shear, sort, typos, rumdl)
just format
just lint # typos, rumdl, cargo sort, fmt, clippy -D warnings, shear
just test # cargo test --all-features (unit + proptest + integration)
just mutation # cargo-mutants — kill surviving mutants
just check-cn # no CJK in code/commentsThe integration suite (bin/shellflow/tests/) runs the real binary against
mock ssh/rsync/scp shims, so no network is required.
Apache-2.0