An interactive step-debugger for Bash scripts - no patched bash, no ptrace, just the DEBUG trap and /dev/tcp. Ships the Rust binary as a console script, ruff-style.
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file trapdoor_sh-0.1.0.tar.gz.
File metadata
- Download URL: trapdoor_sh-0.1.0.tar.gz
- Upload date:
- Size: 16.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36fddfd3b76b0e240adee1f856a3e132b40e9205527628a95947ef5e63c19155
|
|
| MD5 |
3957685e7420277202ebe255ee5e8053
|
|
| BLAKE2b-256 |
2abaf87d8e0954102cbc4a3de3a951b33f86651695c89687b44676968e29e9f7
|
Provenance
The following attestation bundles were made for trapdoor_sh-0.1.0.tar.gz:
Publisher:
pypi.yml on Makeph/trapdoor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
trapdoor_sh-0.1.0.tar.gz -
Subject digest:
36fddfd3b76b0e240adee1f856a3e132b40e9205527628a95947ef5e63c19155 - Sigstore transparency entry: 2275193274
- Sigstore integration time:
-
Permalink:
Makeph/trapdoor@31be0ad20fbc709f7bca366405a77324c9110f9b -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Makeph
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@31be0ad20fbc709f7bca366405a77324c9110f9b -
Trigger Event:
push
-
Statement type:
File details
Details for the file trapdoor_sh-0.1.0-py3-none-win_amd64.whl.
File metadata
- Download URL: trapdoor_sh-0.1.0-py3-none-win_amd64.whl
- Upload date:
- Size: 164.5 kB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
032872c2cb9db6e4255cae1773970fab97a8d9802cc5a0f750409ba772c5ee30
|
|
| MD5 |
0aff57309c8ee52c4876214bab67b5ae
|
|
| BLAKE2b-256 |
2c80feba85082459dbd14d09ff2c3112b511f77c3b98194da1bfa45eb758783d
|
Provenance
The following attestation bundles were made for trapdoor_sh-0.1.0-py3-none-win_amd64.whl:
Publisher:
pypi.yml on Makeph/trapdoor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
trapdoor_sh-0.1.0-py3-none-win_amd64.whl -
Subject digest:
032872c2cb9db6e4255cae1773970fab97a8d9802cc5a0f750409ba772c5ee30 - Sigstore transparency entry: 2275193399
- Sigstore integration time:
-
Permalink:
Makeph/trapdoor@31be0ad20fbc709f7bca366405a77324c9110f9b -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Makeph
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@31be0ad20fbc709f7bca366405a77324c9110f9b -
Trigger Event:
push
-
Statement type:
File details
Details for the file trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl.
File metadata
- Download URL: trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl
- Upload date:
- Size: 235.4 kB
- Tags: Python 3, manylinux: glibc 2.39+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a335cd92e821a51932ea72d7050b7894a8165bb6521e9905c19f21ec535007d
|
|
| MD5 |
b01a09d28a4833cac3329f17ca1bcf8a
|
|
| BLAKE2b-256 |
33de78ed3f411e0c778b4e29b2e162b286e06d9fe446d39095110bcf50548ca7
|
Provenance
The following attestation bundles were made for trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl:
Publisher:
pypi.yml on Makeph/trapdoor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl -
Subject digest:
8a335cd92e821a51932ea72d7050b7894a8165bb6521e9905c19f21ec535007d - Sigstore transparency entry: 2275193503
- Sigstore integration time:
-
Permalink:
Makeph/trapdoor@31be0ad20fbc709f7bca366405a77324c9110f9b -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Makeph
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@31be0ad20fbc709f7bca366405a77324c9110f9b -
Trigger Event:
push
-
Statement type:
File details
Details for the file trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 211.1 kB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
847a02cc50e0958c314dbf8687d6f2d96da3c74e1e5db5d4255981ff2b2cc9b9
|
|
| MD5 |
e535dffff2380a3ff446917fd7cd2441
|
|
| BLAKE2b-256 |
34b1313806713d157ea64e30d503b2cfa8afd750b63ee89efaf263376f74b128
|
Provenance
The following attestation bundles were made for trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl:
Publisher:
pypi.yml on Makeph/trapdoor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl -
Subject digest:
847a02cc50e0958c314dbf8687d6f2d96da3c74e1e5db5d4255981ff2b2cc9b9 - Sigstore transparency entry: 2275193634
- Sigstore integration time:
-
Permalink:
Makeph/trapdoor@31be0ad20fbc709f7bca366405a77324c9110f9b -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Makeph
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@31be0ad20fbc709f7bca366405a77324c9110f9b -
Trigger Event:
push
-
Statement type: