See what your bash script actually runs, line by line, every time you save,
without letting it touch your machine.
Bashle runs your bash script in a throwaway sandbox every time you save, and writes what bash actually ran at the end of each line:
# @probe "reports/Q3 report.txt"
file=$1 file='reports/Q3 report.txt'
rm -rf "$backupdir/partial" rm -rf /partial
cp $file "$backup_dir/" cp reports/Q3 report.txt backup/ ✗1A typo'd variable pointed rm at /partial, and an unquoted $file split in two. You see both
without running anything against your real files.
- Set up: VS Code, Neovim, Vim, the terminal
- Probes: telling bashle how to run a script
- Holes: what happens to network calls and other unknowns
Everywhere you need macOS or Linux and bash 4.1 or newer.
- macOS ships bash 3.2, so run
brew install bash. Bashle finds the new one on its own. - On Linux, install the sandbox:
sudo apt install bubblewrap(ordnf install bubblewrap).
- Cursor, VSCodium: search for Bashle in the Extensions view and install it.
- VS Code 1.85+: download the
.vsixfrom Open VSX, thencode --install-extension <the file>.
Open any .sh file and save it. Notes appear at the end of each line; click Bashle in the status
bar for the panel.
Needs Neovim 0.5+ or Vim 9.0+, plus Node.js. The plugin runs a small CLI that you build once.
With lazy.nvim:
{ "srj31/bashle", build = "npm install && npm run build:cli", ft = "sh" }With vim-plug:
Plug 'srj31/bashle', { 'do': 'npm install && npm run build:cli' }Save a .sh file to run it. Useful commands:
| Command | Keys | Does |
|---|---|---|
:BashleRun |
<Leader>br |
Run the probes (also runs on save) |
:BashleInspect |
<Leader>bi |
Show every run of the current line, in a popup |
:BashlePanel |
<Leader>bp |
Holes, changed files and output in a split |
:BashlePick |
<Leader>bn |
Pick which probe's results to show, or all. 3<Leader>bn jumps to probe 3 |
On Vim 8.2 the panel and popup work, but there are no end-of-line notes.
From a clone of the repository:
npm install
npm run probe -- path/to/script.sh # annotated, in colour
npm run probe -- --json path/to/script.sh # the same, as JSONIt exits with status 1 if any check failed, so it works in CI.
A probe is a comment saying how to run the script. Everything after @probe becomes the script's
arguments, quoted like in a terminal:
#!/usr/bin/env bash
# @probe world
name=$1 name=world
echo "Hello, $name" echo 'Hello, world' name=worldMake it a check by adding =>:
# @probe world => "Hello, world" # passes if it prints exactly this
# @probe staging => exit 0 # passes if it exits 0The result shows on the probe line: ✓ passed in 33 ms or ✗ expected … · got ….
Test one function by putting the probe directly above it, with no blank line:
# @probe "a//b" => "a/b"
normalize_path() {
echo "${1//\/\//\/}"
}Several probes: one per line. Each runs in its own fresh copy.
# @probe staging => exit 0
# @probe prod => exit 1Environment and input apply to every probe in the same comment block:
# @env DEPLOY_ENV=staging
# @stdin "yes"
# @probe --interactiveBashle never makes real network calls or talks to other machines. When a script runs curl,
wget, ssh, docker, kubectl, aws and the like, or reads a file that doesn't exist, bashle
hands back a placeholder called a hole and keeps going. There are two kinds.
The answer is something bashle can't know. You can follow it through the script:
version=$(curl -fsS "https://releases.example.com/latest") version=◇1
dest="releases/$1-$version" dest=releases/staging-◇1Fill it by adding a line to the probe's comment block (the Holes panel and the hover give you this line ready to copy):
# @net GET https://releases.example.com/latest => "1.4.2"
# @probe stagingNow ◇1 reads 1.4.2 everywhere. Other fills work the same way:
| Line | Answers |
|---|---|
# @net GET https://… => "…" |
curl / wget |
# @cmd docker ps => "…" |
ssh, docker, and other commands |
# @file config/app.conf => "…" |
a file the script reads |
# @clock 2026-03-01T09:00:00Z |
what date reports |
After => you can write "text", exit 22, "text" exit 22, or @path/to/file for its contents.
If the command that made the hole also used an empty variable, bashle marks it ◇! and names
the variable instead of offering a fill:
api_url=${API_URL:-} api_url=
curl -fsS "$api_url/health" curl -fsS /health ◇!1 $api_url was empty hereFilling this one would just hide the mistake. Fix the variable.
examples/release.sh shows both kinds, with one probe that leaves the
download open and one that fills it.
- The bashle guide: every option, reading results, safety, troubleshooting
- Using bashle in a real repository:
.bashleignore, settings - How it works · A semantics for holes · Lean proofs
- Contributing
MIT
