trapdoor
An interactive step-debugger for Bash scripts — built entirely out of things bash already ships.
No patched bash. No ptrace. No set -x archaeology. The debugger side of trapdoor is
~60 lines of pure bash injected through BASH_ENV; it talks to a Rust controller over
/dev/tcp, bash's built-in TCP socket. The DEBUG trap does the rest: every command in
your script pauses and asks permission before it runs.
Born from a wish on Ask HN: What developer tool do you wish existed in 2026?:
"No debugging interface for shell scripts — stop at a specific point in the script, modify any commands and execute the step."
So: breakpoints (conditional ones too), step / next / finish, backtraces, source
listings — and a REPL that evaluates inside the live script, so assignments stick.
Change a variable mid-loop and watch the script take the other branch.
Demo
$ trapdoor -b 'demo.sh:13 if (( count == 1 ))' -r examples/demo.sh
breakpoint #1 at demo.sh:13 if (( count == 1 ))
hello, apple
examples/demo.sh:13 [depth 1] ● breakpoint #1
→ count=$((count + 1))
(tdb) p count fruit
declare -- count="1"
declare -- fruit="banana"
(tdb) !count=40
(tdb) c
hello, banana
hello, cherry
processed 43 fruits, total=602
That 43 is not a typo — !count=40 rewrote the loop counter in the running script.
New here? The 15-minute tutorial walks from first step to
patching a live script's variables mid-run.
How it works
┌─────────────────────┐ TCP 127.0.0.1:<random> ┌──────────────────────────┐
│ trapdoor (Rust) │◄───────────────────────────►│ bash your-script.sh │
│ breakpoints, REPL, │ STOP file:line:cmd ───► │ stub via BASH_ENV: │
│ step logic, source │ ◄─── GO / EVAL / BT │ exec {fd}<>/dev/tcp/… │
│ listings, colors │ │ trap '…' DEBUG │
└─────────────────────┘ └──────────────────────────┘
- The controller binds a random localhost port and launches your script with
BASH_ENVpointing at a tiny stub (src/stub.sh). - The stub opens a socket with
exec {fd}<>/dev/tcp/127.0.0.1/$PORT— nonc, nosocat, no python;/dev/tcpis interpreted by bash itself. - It arms the
DEBUGtrap (withset -o functraceso functions inherit it). Before every simple command, the stub reportsfile:line, call depth and the command text, then blocks until the controller answers. GOruns the command.EVAL <code>runs arbitrary bash in the script's own execution context — that's howp,x,!and conditional breakpoints work.BTwalksFUNCNAME/BASH_SOURCE/BASH_LINENOfor a backtrace.- If the controller dies, the stub disarms the trap and the script runs free. No zombie hostages.
Install
cargo install --path . # or: cargo build --release
One static-ish binary, zero crate dependencies (std only). Works anywhere bash is
compiled with /dev/tcp support — Linux distros, macOS, and Git Bash / MSYS2 on
Windows all qualify.
Usage
trapdoor [OPTIONS] <script.sh> [script args...]
-b, --break <SPEC> breakpoint: <line> | <file>:<line> | <file>:<line> if <bash-cond>
-r, --run don't stop at the first command; run until a breakpoint
--bash <PATH> which bash to use
--no-color disable ANSI colors
At the (tdb) prompt:
| command | effect |
|---|---|
s / step |
stop at the next command, anywhere (steps into functions) |
n / next |
next command at this depth or shallower (steps over calls) |
f / finish |
run until the current function returns |
c / continue |
run until a breakpoint |
u <line> |
run until that line in the current file (one-shot) |
| enter | repeat the last motion command |
b 13 · b utils.sh:40 |
breakpoint |
b 13 if (( count == 2 )) |
conditional breakpoint — the condition is raw bash run in the script: (( … )), [[ … ]], even grep -q … |
bl / d <id> |
list / delete breakpoints |
w <bash-expr> |
watch: evaluated in the script and shown at every stop (w $count) |
wl / wd <id> |
list / delete watches |
p var… |
declare -p variables (arrays and maps print properly) |
x code / !code |
run bash in the live script — assignments stick |
bt |
backtrace |
l [line] |
source listing around the current (or given) line |
q |
kill the script and quit |
Honest caveats
- Commands inside
$( … )command substitutions and pipeline segments run in subshells; trapdoor deliberately stays quiet there (two writers on one socket would corrupt the protocol). You still stop on the enclosing command. - The
DEBUGtrap fires per simple command, so a compound likefor …stops once at the loop head, then at each body command — same asbash -xgranularity. - Scripts that read stdin share it with the debugger REPL. Redirect one of them.
- Requires bash ≥ 4.1 (for
{fd}<>auto-allocation) built with/dev/tcp.
Why not bashdb?
bashdb is venerable and more featureful, but it's a 1MB bash-in-bash interpreter you have to install on the target machine. trapdoor's target-side footprint is one temp file and one socket, injected by the environment — nothing to install where the script runs, and the brains stay in one fast binary.
License
MIT
Metadata
Release files for trapdoor-sh 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| trapdoor_sh-0.1.0.tar.gz | 16.3 kB | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| trapdoor_sh-0.1.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl | Python 3 | none | Linux glibc 2.39+ x86-64 | Details |
| trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
Total release size: 627.2 kB
Release files / trapdoor_sh-0.1.0.tar.gz
| Download URL | trapdoor_sh-0.1.0.tar.gz |
|---|---|
| Size | 16.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
36fddfd3b76b0e240adee1f856a3e132b40e9205527628a95947ef5e63c19155
|
|
BLAKE2b-256 checksum How to use checksums |
2abaf87d8e0954102cbc4a3de3a951b33f86651695c89687b44676968e29e9f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency logRelease files / trapdoor_sh-0.1.0-py3-none-win_amd64.whl
| Download URL | trapdoor_sh-0.1.0-py3-none-win_amd64.whl |
|---|---|
| Size | 164.5 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
032872c2cb9db6e4255cae1773970fab97a8d9802cc5a0f750409ba772c5ee30
|
|
BLAKE2b-256 checksum How to use checksums |
2c80feba85082459dbd14d09ff2c3112b511f77c3b98194da1bfa45eb758783d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency logRelease files / trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl
| Download URL | trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl |
|---|---|
| Size | 235.4 kB |
| Tags | Linux glibc 2.39+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
8a335cd92e821a51932ea72d7050b7894a8165bb6521e9905c19f21ec535007d
|
|
BLAKE2b-256 checksum How to use checksums |
33de78ed3f411e0c778b4e29b2e162b286e06d9fe446d39095110bcf50548ca7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency logRelease files / trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl
| Download URL | trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 211.1 kB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
847a02cc50e0958c314dbf8687d6f2d96da3c74e1e5db5d4255981ff2b2cc9b9
|
|
BLAKE2b-256 checksum How to use checksums |
34b1313806713d157ea64e30d503b2cfa8afd750b63ee89efaf263376f74b128
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency log