LocalBash plugin
Use LocalBash for an application command, worker or test script that runs on the same machine as your Reflow CLI. Each invocation keeps its output and exit status as evidence. Use DockerUbuntu for commands inside an existing Ubuntu container.
Connect
Section titled “Connect”Select your application collection during onboarding. Use its ID in the target command below.
Install the bundled plugin with reflow init --collection <collection-id>. The plugin
bundles GNU Bash 5.3; your command’s own dependencies must be available on the machine.
Save this private connection as bindings.json, replacing the directory with your
application’s absolute path:
{ "worker": { "provider": "LocalBash", "configuration": {"cwd": "/absolute/path/to/your/app"} }}cwd is required and must exist; it is the only configuration key. Commands run
on the bundled Bash without interactive profile or rc files. Use explicit commands; aliases from your interactive shell are not loaded.
Keep the bindings file out of Git.
chmod 600 /absolute/path/to/bindings.jsonreflow target --group worker-tests --collection <collection-id> --bindings /absolute/path/to/bindings.jsonFor manage_device_jobs runs, select this same application and private bindings
before starting the saved flow. The job checks the application’s plugin lock and
each declared binding, then copies the selected configuration into its private
worker. The binding values stay on the device; shared shell evidence redacts known
private values. reflow init installs plugins; reflow target --bindings selects
their private configuration.
This also applies to explicit Browser aliases such as providers: {site: Browser}:
select a site entry with provider: "Browser" and configuration: {} to use
defaults. Implicit Browser statements without a provider declaration need no
private binding. Device jobs select bindings by application and authenticated
connection.
The owner-only configuration copy stays in the local evidence slot until that slot
is discarded; evidence reads and review exports do not include that file.
Write a flow
Section titled “Write a flow”Copy this into /tmp/worker-output.md, outside your checkout.
This example needs only Bash:
---name: Capture command outputproviders: worker: LocalBash---
### Capture command output
```rflworker.bash command="printf 'worker ready\n'" checkpoint="worker-output"```Create the test in the team’s chosen location, then run it:
reflow flows create --name worker-output --file /tmp/worker-output.mdreflow plan worker-outputreflow apply worker-outputreflow dashboardReplace the printf command with inline test logic or the application executable under test.
A saved test must run on another machine: keep setup, assertions and cleanup in the
saved flow, and carry helper scripts as flow files.
Commands may run the application’s executables and files committed to its repository,
never a script that exists only in your working directory, home directory or a private
binding. Credentials and endpoints stay in private bindings. Each command accepts at most
65536 UTF-8 bytes; split larger tests into bounded steps.
The alias worker selects the private connection with the same name. See
plugin setup for aliases and version pins.
Carry helper scripts in the flow
Section titled “Carry helper scripts in the flow”Declare a helper as a fenced block whose info string is file="relative/path",
optionally preceded by a language for highlighting. LocalBash receives every
declared file in a fresh private directory named by REFLOW_FLOW_FILES:
---name: Order totalsproviders: worker: LocalBash---
```python file="check_totals.py"import json, subprocessorders = json.loads(subprocess.check_output(["./bin/list-orders", "--json"]))assert sum(o["total"] for o in orders) == 4200, ordersprint("totals ok")```
### Check totals
```rflworker.bash command="python3 \"$REFLOW_FLOW_FILES/check_totals.py\"" checkpoint="totals"```- The file’s bytes are the block body exactly. The opening fence starts at the beginning of its line and the block is closed; a body line of three backticks, optionally surrounded by whitespace, would close it early.
- Paths use letters, digits,
.,_,-and/, with no.or..segment. Paths may not differ only in letter case, and a file cannot also be a directory. A flow declares at most 8 files, each at most 64 KiB and 192 KiB in total; the whole flow, files included, stays within its 256 KiB source limit. - The directory is not the working directory. Commands still run in the
binding’s
cwd; refer to files as"$REFLOW_FLOW_FILES/<path>". Write$REFLOW_FLOW_FILESwithout braces: RFL interpolates${…}in plugin arguments. - Files are read-only, written before the first LocalBash command and removed after
the run.
files=captures outputs belowcwdand cannot capture them. - Editing a file changes the saved flow, so plan/apply reruns its steps instead of reusing earlier results.
On the author device, saving refuses a test whose LocalBash commands run as a
program or interpreter script, source, read with < or copy a local file that
another machine would not have, and names the path and the fix:
- Tests kept in Reflow carry helpers as flow files. Application files they use must be committed to the application repository.
- Tests kept in the repository may instead commit helpers beside the test, for
example
.reflow/flows/order-totals/check_totals.py, staged with it. - Refused: files that are not committed (or staged, for repository tests), missing, outside the checkout (including your home directory), ignored by Git outside build and dependency directories, or reached through an absolute path into the checkout (write it relative to the working directory instead).
- Fine: installed runtimes and tools, paths the flow writes before it uses them
(such as heredoc output), and build or dependency output such as
dist/,target/,.venv/ornode_modules/.
Other arguments, such as jq . data.json, are not inspected. A hosted
connection has no checkout: it refuses invalid flow files and returns unverified
paths as portability_warnings.
Commands
Section titled “Commands”worker.bash accepts:
| Argument | Type | Meaning |
|---|---|---|
command | string, required | Bash source to run in the configured directory. |
files | string, optional | A JSON array of relative output file paths to retain after the command. It never supplies inputs. |
checkpoint is a host RFL modifier for the invocation’s retained evidence, not a
LocalBash command argument. RFL removes it before provider argument validation and
dispatch, so it is absent from the bash descriptor returned by list_plugins.
Use a literal label of 1-128 letters, digits, underscores or hyphens. Other unknown
arguments are still rejected. It is not a separate shell command or a screenshot
of a terminal.
For a command that writes a report:
worker.bash command="./bin/check-orders > report.txt" files="[\"report.txt\"]" checkpoint="order-report"That example assumes your application supplies ./bin/check-orders. Selected files
must exist inside cwd; absolute paths and paths that escape it are rejected.
Each captured file is limited to 4 MiB. Stdout and stderr are each limited to 1 MiB and indicate truncation when it
occurs. A truncated result cannot establish full equality during comparison.
Escape shell parameter expansion
Section titled “Escape shell parameter expansion”In a quoted plugin argument, write $${ for a literal ${. RFL consumes the
extra dollar once, then Bash receives the original shell parameter expansion.
This applies to both LocalBash and DockerUbuntu, including ${PIPESTATUS[0]},
${VAR:-default} and ${#arr[@]}. Shell quotes alone do not stop RFL interpolation.
worker.bash command="unset VAR; printf '%s\\n' \"$${VAR:-default}\""worker.bash command="arr=(one two); printf '%s\\n' \"$${#arr[@]}\""worker.bash command="false | true; code=$${PIPESTATUS[0]}; printf '%s\\n' \"$code\"; exit \"$code\""The first two commands print default and 2. The pipeline example prints 1
and deliberately fails with exit status 1, retaining the first command’s failure.
For nested shell expansions, escape each opening: $${VAR:-$${OTHER:-default}}.
An ordinary ${inputs.name} still resolves a Reflow input in the same command.
Use double-quoted RFL strings and JSON escapes for quotes/backslashes. \${ is
not a supported RFL escape; $${ is the literal-opening syntax. This is separate
from shell quoting: to prevent Bash from expanding the resulting ${...}, quote
it appropriately in the shell command too. See the
language reference.
Keep generated reports outside the checkout
Section titled “Keep generated reports outside the checkout”cwd does not have to be your application checkout. For a knowledge-base PoC,
create a private work directory outside the repository and use its absolute path
as the LocalBash cwd. Stdout and stderr are retained automatically without
creating report files in the checkout. If you need a captured report file, run
the application command in a subshell and redirect its output from the private
working directory:
worker.bash command="(cd '/absolute/path/to/your/app' && ./bin/check-orders) > report.txt" files="[\"report.txt\"]" checkpoint="order-report"Replace the path and command with your application’s actual command. The subshell
changes only the command’s working directory; report.txt is written under the
configured private cwd. The command itself can still create application files,
so use its own output-directory option when appropriate. Capture does not follow
symlinks that escape cwd; an external symlink is not a supported workaround.
There is currently no separate evidenceDir configuration.
Evidence and failures
Section titled “Evidence and failures”The snapshot retains stdout, stderr and exit code, plus requested files. A nonzero
exit is a failed result whose output remains available for diagnosis. No additional
snapshot command is needed. Comparing two results uses the retained outputs and
file hashes; a successful exit does not mean their contents are unchanged.
Shared shell evidence redacts known private values from text and withholds affected binary files. Local files remain unchanged.
Use snapshots and review to inspect changes, and repair and rerun when a command or its expected result changes.
Flows using this plugin can be saved in Reflow or in your repository. Read test storage for the team preference, private bindings and the shared plan/apply workflow.
Keep managed consent hooks in the flow
Section titled “Keep managed consent hooks in the flow”For MCP tests using managed consent identity, private bindings can select
setCommand: "@flow:set" and readCommand: "@flow:read" together. Put the complete
hook source in the saved flow’s consent_hooks frontmatter, keyed by the binding
name, with setCommand and readCommand strings. Each accepts at most 8192
characters. Hooks do not interpolate RFL inputs: read private fixture data from
the configured working directory and the requested identity from the private
REFLOW_PRIVATE_IDENTITY_FILE JSON file. Keep identities and credentials outside
shared source. Planning validates the declaration without executing it; execution
uses the admitted source with the existing identity lock, verification and restore
lifecycle. Missing or mixed declarations refuse rather than using another source.