Skip to main content

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         │
└─────────────────────┘                             └──────────────────────────┘
  1. The controller binds a random localhost port and launches your script with BASH_ENV pointing at a tiny stub (src/stub.sh).
  2. The stub opens a socket with exec {fd}<>/dev/tcp/127.0.0.1/$PORT — no nc, no socat, no python; /dev/tcp is interpreted by bash itself.
  3. It arms the DEBUG trap (with set -o functrace so functions inherit it). Before every simple command, the stub reports file:line, call depth and the command text, then blocks until the controller answers.
  4. GO runs the command. EVAL <code> runs arbitrary bash in the script's own execution context — that's how p, x, ! and conditional breakpoints work. BT walks FUNCNAME/BASH_SOURCE/BASH_LINENO for a backtrace.
  5. 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 DEBUG trap fires per simple command, so a compound like for … stops once at the loop head, then at each body command — same as bash -x granularity.
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

trapdoor_sh-0.1.0.tar.gz (16.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

trapdoor_sh-0.1.0-py3-none-win_amd64.whl (164.5 kB view details)

Uploaded Python 3Windows x86-64

trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl (235.4 kB view details)

Uploaded Python 3manylinux: glibc 2.39+ x86-64

trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl (211.1 kB view details)

Uploaded Python 3macOS 11.0+ ARM64

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

Hashes for trapdoor_sh-0.1.0.tar.gz
Algorithm Hash digest
SHA256 36fddfd3b76b0e240adee1f856a3e132b40e9205527628a95947ef5e63c19155
MD5 3957685e7420277202ebe255ee5e8053
BLAKE2b-256 2abaf87d8e0954102cbc4a3de3a951b33f86651695c89687b44676968e29e9f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for trapdoor_sh-0.1.0.tar.gz:

Publisher: pypi.yml on Makeph/trapdoor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for trapdoor_sh-0.1.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 032872c2cb9db6e4255cae1773970fab97a8d9802cc5a0f750409ba772c5ee30
MD5 0aff57309c8ee52c4876214bab67b5ae
BLAKE2b-256 2c80feba85082459dbd14d09ff2c3112b511f77c3b98194da1bfa45eb758783d

See more details on using hashes here.

Provenance

The following attestation bundles were made for trapdoor_sh-0.1.0-py3-none-win_amd64.whl:

Publisher: pypi.yml on Makeph/trapdoor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for trapdoor_sh-0.1.0-py3-none-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 8a335cd92e821a51932ea72d7050b7894a8165bb6521e9905c19f21ec535007d
MD5 b01a09d28a4833cac3329f17ca1bcf8a
BLAKE2b-256 33de78ed3f411e0c778b4e29b2e162b286e06d9fe446d39095110bcf50548ca7

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for trapdoor_sh-0.1.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 847a02cc50e0958c314dbf8687d6f2d96da3c74e1e5db5d4255981ff2b2cc9b9
MD5 e535dffff2380a3ff446917fd7cd2441
BLAKE2b-256 34b1313806713d157ea64e30d503b2cfa8afd750b63ee89efaf263376f74b128

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page