node-inspect-debugger
Debug Node.js via --inspect + Chrome DevTools Protocol CLI.
Debug Node.js via --inspect + Chrome DevTools Protocol CLI.
Real data. Real impact.
Emerging
Developers
Per week
Excellent
Skills give you superpowers. Install in 30 seconds.
When
console.log isn't enough, drive Node's built-in V8 inspector programmatically from the terminal. You get real breakpoints, step in/over/out, call-stack walking, local/closure scope dumps, and arbitrary expression evaluation in the paused frame.
Two tools, pick one:
node inspect — built-in, zero install, CLI REPL. Best for quick poking.ndb / CDP via chrome-remote-interface — scriptable from Node/Python; best when you want to automate many breakpoints, collect state across runs, or debug non-interactively from an agent loop.Prefer
first. It's always available and the REPL is fast.node inspect
_SlashWorker, PTY bridge workers) misbehaveconsole.log can't reach without patchingDon't use for: things
console.log solves in under a minute. Breakpoint-driven debugging is heavier; use it when the payoff is real.
node inspect REPLLaunch paused on first line:
node inspect path/to/script.js # or with tsx node --inspect-brk $(which tsx) path/to/script.ts
The
debug> prompt accepts:
| Command | Action |
|---|---|
or | continue |
or | step over |
or | step into |
or | step out |
| pause running code |
| set breakpoint at file.js line 42 |
| set breakpoint at line 42 of current file |
| break when function is called |
| clear breakpoint |
| list all breakpoints |
| backtrace (call stack) |
| show 5 lines of source around current position |
| evaluate expr on every pause |
| show watched expressions |
| drop into REPL in current scope (Ctrl+C to exit REPL) |
| evaluate expression once |
| restart script |
| kill the script |
| quit debugger |
In the
sub-mode: type any JS expression, including access to locals/closure variables. repl
Ctrl+C exits back to debug>.
When the process is already running (e.g. a long-lived dev server or the TUI gateway):
# 1. Send SIGUSR1 to enable the inspector on an existing process kill -SIGUSR1 <pid> # Node prints: Debugger listening on ws://127.0.0.1:9229/<uuid> # 2. Attach the debugger CLI node inspect -p <pid> # or by URL node inspect ws://127.0.0.1:9229/<uuid>
To start a process with the inspector from the beginning:
node --inspect script.js # listen on 127.0.0.1:9229, keep running node --inspect-brk script.js # listen AND pause on first line node --inspect=0.0.0.0:9230 script.js # custom host:port
For TypeScript via tsx:
node --inspect-brk --import tsx script.ts # or older tsx node --inspect-brk -r tsx/cjs script.ts
When you want to automate — set many breakpoints, capture scope state, script a repro — use
chrome-remote-interface:
npm i -g chrome-remote-interface # or project-local # Start your target: node --inspect-brk=9229 target.js &
Driver script (save as
/tmp/cdp-debug.js):
const CDP = require('chrome-remote-interface'); (async () => { const client = await CDP({ port: 9229 }); const { Debugger, Runtime } = client; Debugger.paused(async ({ callFrames, reason }) => { const top = callFrames[0]; console.log(`PAUSED: ${reason} @ ${top.url}:${top.location.lineNumber + 1}`); // Walk scopes for locals for (const scope of top.scopeChain) { if (scope.type === 'local' || scope.type === 'closure') { const { result } = await Runtime.getProperties({ objectId: scope.object.objectId, ownProperties: true, }); for (const p of result) { console.log(` ${scope.type}.${p.name} =`, p.value?.value ?? p.value?.description); } } } // Evaluate an expression in the paused frame const { result } = await Debugger.evaluateOnCallFrame({ callFrameId: top.callFrameId, expression: 'typeof state !== "undefined" ? JSON.stringify(state) : "n/a"', }); console.log('state =', result.value ?? result.description); await Debugger.resume(); }); await Runtime.enable(); await Debugger.enable(); // Set a breakpoint by URL regex + line await Debugger.setBreakpointByUrl({ urlRegex: '.*app\\.tsx$', lineNumber: 119, // 0-indexed columnNumber: 0, }); await Runtime.runIfWaitingForDebugger(); })();
Run it:
node /tmp/cdp-debug.js
Hermes-specific note:
chrome-remote-interface is NOT in ui-tui/package.json. Install it to a throwaway location if you don't want to dirty the project:
mkdir -p /tmp/cdp-tools && cd /tmp/cdp-tools && npm i chrome-remote-interface NODE_PATH=/tmp/cdp-tools/node_modules node /tmp/cdp-debug.js
The TUI is built Ink + tsx. Two common scenarios:
ui-tui/package.json has npm run dev (tsx --watch). Add --inspect-brk by running tsx directly:
cd /home/bb/hermes-agent/ui-tui npm run build # produce dist/ once so transpile isn't needed on first load node --inspect-brk dist/entry.js # In another terminal: node inspect -p <node pid>
Then inside
debug>:
sb('dist/app.js', 220) # or wherever the suspect render is cont
When it pauses,
repl → inspect props, state refs, useInput handler values, etc.
hermes --tuiThe TUI spawns Node from the Python CLI. Easiest path:
# 1. Launch TUI hermes --tui & TUI_PID=$(pgrep -f 'ui-tui/dist/entry' | head -1) # 2. Enable inspector on that Node PID kill -SIGUSR1 "$TUI_PID" # 3. Find the WS URL curl -s http://127.0.0.1:9229/json/list | jq -r '.[0].webSocketDebuggerUrl' # 4. Attach node inspect ws://127.0.0.1:9229/<uuid>
Interacting with the TUI (typing in its window) continues to advance execution; your debugger can pause it on a breakpoint at any
sb(...).
_SlashWorker / PTY child processesThose are Python, not Node — use the
python-debugpy skill for them. Only Node portions (Ink UI, tui_gateway client, tsx-run tests under ui-tui/) use this skill.
cd /home/bb/hermes-agent/ui-tui # Run a single test file paused on entry node --inspect-brk ./node_modules/vitest/vitest.mjs run --no-file-parallelism src/app/foo.test.tsx
In another terminal:
node inspect -p <pid>, then sb('src/app/foo.tsx', 42), cont.
Use
--no-file-parallelism (vitest) or --runInBand (jest) so only one worker exists — debugging a pool is painful.
From the CDP driver above, swap Debugger for
HeapProfiler / Profiler:
// CPU profile for 5 seconds await client.Profiler.enable(); await client.Profiler.start(); await new Promise(r => setTimeout(r, 5000)); const { profile } = await client.Profiler.stop(); require('fs').writeFileSync('/tmp/cpu.cpuprofile', JSON.stringify(profile)); // Open /tmp/cpu.cpuprofile in Chrome DevTools → Performance tab
// Heap snapshot await client.HeapProfiler.enable(); const chunks = []; client.HeapProfiler.addHeapSnapshotChunk(({ chunk }) => chunks.push(chunk)); await client.HeapProfiler.takeHeapSnapshot({ reportProgress: false }); require('fs').writeFileSync('/tmp/heap.heapsnapshot', chunks.join(''));
Wrong line numbers in TS source. Breakpoints hit the emitted JS, not the
.ts. Either (a) break in the built dist/*.js, or (b) enable sourcemaps (node --enable-source-maps) and use sb('src/app.tsx', N) — but only with CDP clients that follow sourcemaps. node inspect CLI does not.
vs --inspect
. --inspect-brk
--inspect starts the inspector but doesn't pause; your script races past your first breakpoint if you attach too late. Use --inspect-brk when you need to set breakpoints before any code runs.
Port collisions. Default is
9229. If multiple Node processes are inspecting, pass --inspect=0 (random port) and read the actual URL from /json/list:
curl -s http://127.0.0.1:9229/json/list # lists all inspectable targets on the host
Child processes.
--inspect on a parent does NOT inspect its children. Use NODE_OPTIONS='--inspect-brk' node parent.js to propagate to every child; be aware they all need unique ports (Node auto-increments when NODE_OPTIONS='--inspect' is inherited).
Background kills. If you
Ctrl+C out of node inspect while the target is paused, the target stays paused. Either cont first, or kill the target explicitly.
Running
through an agent terminal. It's a PTY-friendly REPL. In Hermes, launch it with node inspect
terminal(pty=true) or background=true + process(action='submit', data='...'). Non-PTY foreground mode will work for one-shot commands but not for interactive stepping.
Security.
--inspect=0.0.0.0:9229 exposes arbitrary code execution. Always bind to 127.0.0.1 (the default) unless you have an isolated network.
After setting up a debug session, verify:
curl -s http://127.0.0.1:9229/json/list returns exactly the target you expect--inspect-brk or attached after execution completed)exec process.pid in repl returns the PID you meant to attach to"Why is this variable undefined at line X?"
node --inspect-brk script.js & node inspect -p $! # debug> sb('script.js', X) cont # paused. Now: repl > myVariable > Object.keys(this)
"What's the call path into this function?"
debug> sb('suspectFn') debug> cont # paused on entry debug> bt
"This async chain hangs — where?"
# Start with --inspect (no -brk), let it run to the hang, then: debug> pause debug> bt # Now you see the stuck frame
MIT
mkdir -p ~/.hermes/skills/software-development/node-inspect-debugger && curl -o ~/.hermes/skills/software-development/node-inspect-debugger/SKILL.md https://raw.githubusercontent.com/NousResearch/hermes-agent/main/skills/software-development/node-inspect-debugger/SKILL.md1,500+ AI skills, agents & workflows. Install in 30 seconds. Part of the Torly.ai family.
© 2026 Torly.ai. All rights reserved.