| name | node-inspect-debugger |
| description | Drives Node's V8 inspector from the terminal: node inspect / --inspect-brk, ndb, or chrome-remote-interface for breakpoints, scopes, watches, and CPU/heap. Use when console.log cannot reach closure state or a Node test/TUI worker needs step-through. Not for the four-phase methodology (systematic-debugging), Chrome DOM debugging, or Python debugpy. Never leave --inspect bound on a public interface. |
| version | 1.0.1 |
| author | Hermes Agent |
| license | MIT |
| platforms | ["linux","macos","windows"] |
| metadata | {"hermes":{"tags":["debugging","nodejs","node-inspect","cdp","breakpoints","ui-tui"],"related_skills":["systematic-debugging","python-debugpy","debugging-hermes-tui-commands"]}} |
Node.js Inspect Debugger
Overview
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 node inspect first. It's always available and the REPL is fast.
When to Use
- A Node test fails and you need to see intermediate state
- ui-tui crashes or behaves wrong and you want to inspect React/Ink state pre-render
- tui_gateway child processes (
_SlashWorker, PTY bridge workers) misbehave
- You need to inspect a value in a closure that
console.log can't reach without patching
- Perf: attach to a running process to capture a CPU profile or heap snapshot
Don't use for: things console.log solves in under a minute. Breakpoint-driven debugging is heavier; use it when the payoff is real.
Prerequisites
- Node.js installed and on PATH (verify with
node --version)
- For TypeScript debugging:
tsx installed (npm i -g tsx or project-local)
- For CDP scripting:
chrome-remote-interface — install to a throwaway location to avoid dirtying the project (see Procedure step 6)
- On Windows (PowerShell primary): use
Stop-Process / Get-Process instead of kill/pgrep; use $env:NODE_OPTIONS instead of NODE_OPTIONS=... prefix syntax
- On Linux/macOS:
kill -SIGUSR1 and pgrep are available natively
Procedure
1. Launch a script paused on first line
node inspect path/to/script.js
node --inspect-brk $(which tsx) path/to/script.ts
Windows (PowerShell):
node inspect path\to\script.js
# or with tsx
node --inspect-brk (Get-Command tsx).Source path\to\script.ts
The debug> prompt accepts:
| Command | Action |
|---|
c or cont | continue |
n or next | step over |
s or step | step into |
o or out | step out |
pause | pause running code |
sb('file.js', 42) | set breakpoint at file.js line 42 |
sb(42) | set breakpoint at line 42 of current file |
sb('functionName') | break when function is called |
cb('file.js', 42) | clear breakpoint |
breakpoints | list all breakpoints |
bt | backtrace (call stack) |
list(5) | show 5 lines of source around current position |
watch('expr') | evaluate expr on every pause |
watchers | show watched expressions |
repl | drop into REPL in current scope (Ctrl+C to exit REPL) |
exec expr | evaluate expression once |
restart | restart script |
kill | kill the script |
.exit | quit debugger |
In the repl sub-mode: type any JS expression, including access to locals/closure variables. Ctrl+C exits back to debug>.
2. Attach to a running process
When the process is already running (e.g. a long-lived dev server or the TUI gateway):
kill -SIGUSR1 <pid>
node inspect -p <pid>
node inspect ws://127.0.0.1:9229/<uuid>
Windows (PowerShell) — SIGUSR1 is not available on Windows. Instead, start the process with --inspect from the beginning:
# Start with inspector enabled
node --inspect script.js
# In another terminal, find the PID
Get-Process node | Select-Object Id, ProcessName
# Attach
node inspect -p <pid>
To start a process with the inspector from the beginning:
node --inspect script.js
node --inspect-brk script.js
node --inspect=0.0.0.0:9230 script.js
For TypeScript via tsx:
node --inspect-brk --import tsx script.ts
node --inspect-brk -r tsx/cjs script.ts
3. Debug Hermes ui-tui components
The TUI is built with Ink + tsx. Two common scenarios:
Debugging a single Ink component under dev:
ui-tui/package.json has npm run dev (tsx --watch). Add --inspect-brk by running tsx directly:
cd ui-tui
npm run build
node --inspect-brk dist/entry.js
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.
Debugging a running hermes --tui:
The TUI spawns Node from the Python CLI. Easiest path:
hermes --tui &
TUI_PID=$(pgrep -f 'ui-tui/dist/entry' | head -1)
kill -SIGUSR1 "$TUI_PID"
curl -s http://127.0.0.1:9229/json/list | jq -r '.[0].webSocketDebuggerUrl'
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(...).
Debugging _SlashWorker / PTY child processes:
Those 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.
4. Run Vitest tests under the debugger
cd ui-tui
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.
5. Capture heap snapshots and CPU profiles (non-interactive)
From the CDP driver (step 6), swap Debugger for HeapProfiler / Profiler:
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));
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(''));
6. Programmatic CDP (scripting from terminal)
When you want to automate — set many breakpoints, capture scope state, script a repro — use chrome-remote-interface:
npm i -g chrome-remote-interface
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}`);
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.?. ?? p.?.);
}
}
}
{ result } = .({
: top.,
: ,
});
.(, result. ?? result.);
.();
});
.();
.();
.({
: ,
: ,
: ,
});
.();
})();
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
Windows (PowerShell):
New-Item -ItemType Directory -Force -Path "$env:TEMP\cdp-tools"
Set-Location "$env:TEMP\cdp-tools"
npm i chrome-remote-interface
$env:NODE_PATH = "$env:TEMP\cdp-tools\node_modules"
node "$env:TEMP\cdp-debug.js"
One-Shot Recipes
"Why is this variable undefined at line X?"
node --inspect-brk script.js &
node inspect -p $!
sb('script.js', X)
cont
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
Pitfalls
-
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.
-
--inspect vs --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
-
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). On Windows PowerShell: $env:NODE_OPTIONS='--inspect-brk'; node parent.js.
-
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 node inspect through an agent terminal. It's a PTY-friendly REPL. In Hermes, launch it with 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.
-
Windows SIGUSR1 unavailable. On Windows you cannot send to enable the inspector on a running process. You must start the process with or from the beginning. Plan accordingly for long-lived Windows processes.
Verification
After setting up a debug session, verify each item:
-
Inspector endpoint is live — run:
curl -s http://127.0.0.1:9229/json/list
Confirm it returns exactly the target you expect (correct script path, matching PID).
-
First breakpoint actually hits — if it doesn't, you likely missed --inspect-brk or attached after execution completed. Re-launch with --inspect-brk.
-
Source listing at pause shows the right file — run list(5) at the debug> prompt. A mismatch means a sourcemap issue (see Pitfall 1).
-
Correct process attached — run exec process.pid in repl mode and confirm the returned PID matches the one you intended to attach to.
-
Scope variables are accessible — in repl mode, run Object.keys(this) and check that expected local/closure variables appear.
Related Skills
- systematic-debugging — general debugging methodology and root-cause analysis workflow
- python-debugpy — for Python child processes (
_SlashWorker, PTY bridge workers) that this skill does not cover
- debugging-hermes-tui-commands — Hermes-specific TUI debugging commands and terminal integration