Hi. I've been debugging a fairly large Lua 5.1 codebase at work and got tired of sprinkling print() everywhere, so I ended up writing a proper debugger and just open-sourced it.
(Heads up: most of the code was written with Claude. I drove the design, did the level-discipline debugging, and tested it against our actual codebase — but credit where it's due, the AI did a lot of the typing.)
The whole thing is two files you drop into a project. No luasocket, no luaposix, no C extensions. The child-side stub is plain Lua 5.1 and attaches via LUA_INIT. The controller is LuaJIT and talks to the child over a pair of FIFOs. There's a small CLI
(bin/luaprobe) that wraps the library so you can use it like gdb:
bin/luaprobe -b demo.lua:7 demo.lua
bin/luaprobe -b 'demo.lua:7 if i > 1' demo.lua
It does most of the obvious things: stop and log-only breakpoints, snap-forward (so you don't have to pick a line that the compiler actually emitted opcodes for), step / next / finish / continue, full stack walks with locals and upvalues, deep table dumps with cycle handling, breakpoints that fire inside coroutines, and live add/remove of breakpoints during a pause.
A couple of things I find genuinely useful:
Conditional breakpoints — foo.lua:42 if user.id == target_id. The condition is a Lua expression evaluated against the frame's locals/upvalues with _G as fallback. Typos in the condition just silently never fire
Eval during pause — at the prompt, e some_expression runs in the paused frame's scope. Locals, upvalues, and globals are all visible. Useful when you want to poke at a value without setting a new breakpoint.
Entry-time snapshots — alongside each frame's current locals you also get the values they had on function entry. Helps when you're trying to figure out "wait, was this nil when we got called, or did we clobber it ourselves?"
A few honest caveats: it's Linux-only (uses the O_RDWR | O_NONBLOCK FIFO trick), the line hook adds 2-4x slowdown during a debug session, breakpoints only snap forward, eval is read-only on locals (writes don't persist back to the frame), and coroutines created by C code via lua_newthread are invisible.
The single most error-prone part of writing a Lua debugger turned out to be stack-level discipline — debug.sethook only guarantees getinfo(2) is user code if the hook is the directly-registered function, so any wrapper between sethook and the body silently shifts every level by one and you spend a day wondering why your locals are full of stub internals. There's a section in DEBUGGER.md about that, and the comments in luaprobe_stub.lua are pretty paranoid about it.
MIT-licensed. Happy to answer questions or take suggestions.