Node Inspect Debugger: Drive V8 Inspector from the Terminal
Debug Node.js via --inspect + Chrome DevTools Protocol CLI.
Written by Neura Market from the official Hermes Agent documentation for Node Inspect Debugger. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationWhen console.log stops being enough, you can drive Node's built-in V8 inspector directly from the terminal. This skill shows you how to set real breakpoints, step through code, walk the call stack, dump local and closure scopes, and evaluate arbitrary expressions in a paused frame. It is bundled with Hermes Agent, so it is available by default, and it is the tool you reach for when you need to see what is actually happening inside a running Node process, especially the Ink-based TUI or its child processes.
What it does
This skill gives you two ways to debug Node.js programmatically. The first is node inspect, a zero-install CLI REPL that ships with Node itself. It is fast, always available, and perfect for quick poking. The second is ndb or CDP via chrome-remote-interface, which lets you script debugging from Node or Python. That approach shines when you need to automate many breakpoints, collect state across runs, or debug non-interactively from an agent loop.
The skill covers the full workflow: launching a script paused on the first line, attaching to an already-running process, setting and clearing breakpoints, stepping through code, inspecting scopes, evaluating expressions, and even capturing CPU profiles and heap snapshots. It also includes Hermes-specific recipes for debugging the ui-tui, its Ink components, and Vitest tests.
Before you start
This skill is bundled with Hermes Agent, so it is installed by default. It works on Linux, macOS, and Windows. You need Node.js installed, since both node inspect and chrome-remote-interface depend on it. For TypeScript projects, you will also need tsx if you want to debug .ts files directly. No other installation is required for the node inspect path; chrome-remote-interface is an npm package you install separately.
Quick Reference: node inspect REPL
Launch a script paused on its first line:
node inspect path/to/script.js
# or with tsx
node --inspect-brk $(which tsx) path/to/script.ts
The debug> prompt accepts these commands:
| 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, you can type any JS expression, including access to locals and closure variables. Ctrl+C exits back to debug>.
Attaching to a Running Process
When the process is already running, such as a long-lived dev server or the TUI gateway, you can enable the inspector on it and attach:
# 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
Programmatic CDP (scripting from terminal)
When you want to automate debugging, set many breakpoints, capture scope state, or script a repro, use chrome-remote-interface. Install it globally or project-locally, then start your target:
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
Debugging Hermes ui-tui
The TUI is built with Ink and 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 <hermes-agent-repo>/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.
Debugging a running hermes --tui
The 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(...).
Debugging _SlashWorker / PTY child processes
Those are Python, not Node, so use the python-debugpy skill for them. Only Node portions (Ink UI, tui_gateway client, tsx-run tests under ui-tui/) use this skill.
Running Vitest Tests Under the Debugger
cd <hermes-agent-repo>/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 , then sb('src/app/foo.tsx', 42), cont.
Use --no-file-parallelism (vitest) or --runInBand (jest) so only one worker exists, because debugging a pool is painful.
Heap Snapshots & CPU Profiles (Non-interactive)
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(''));
Common Pitfalls
- Wrong line numbers in TS source. Breakpoints hit the emitted JS, not the
.ts. Either (a) break in the builtdist/*.js, or (b) enable sourcemaps (node --enable-source-maps) and usesb('src/app.tsx', N), but only with CDP clients that follow sourcemaps.node inspectCLI does not. --inspectvs--inspect-brk.--inspectstarts the inspector but doesn't pause; your script races past your first breakpoint if you attach too late. Use--inspect-brkwhen 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.
--inspecton a parent does NOT inspect its children. UseNODE_OPTIONS='--inspect-brk' node parent.jsto propagate to every child; be aware they all need unique ports (Node auto-increments whenNODE_OPTIONS='--inspect'is inherited). - Background kills. If you
Ctrl+Cout ofnode inspectwhile the target is paused, the target stays paused. Eithercontfirst, orkillthe target explicitly. - Running
node inspectthrough an agent terminal. It's a PTY-friendly REPL. In Hermes, launch it withterminal(pty=true)orbackground=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:9229exposes arbitrary code execution. Always bind to127.0.0.1(the default) unless you have an isolated network.
Verification Checklist
After setting up a debug session, verify:
curl -s http://127.0.0.1:9229/json/listreturns exactly the target you expect- First breakpoint actually hits (if it doesn't, you likely missed
--inspect-brkor attached after execution completed) - Source listing at pause shows the right file (mismatch = sourcemap issue, see pitfall 1)
exec process.pidinreplreturns the PID you meant to attach to
One-Shot Recipes
"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
When not to use it
Do not reach for this when console.log solves the problem in under a minute. Breakpoint-driven debugging is heavier; use it only when the payoff is real. Also, this skill is for Node.js only. If you are debugging Python processes like _SlashWorker or PTY bridge workers, use the python-debugpy skill instead.
Limits and gotchas
The source lists several limitations you should keep in mind. The node inspect CLI does not follow sourcemaps, so breakpoints in TypeScript source will not hit as expected unless you use a CDP client that supports sourcemaps. The --inspect flag does not pause execution, so you can miss your first breakpoint if you attach too late; --inspect-brk is the safer choice. Port 9229 is the default, and collisions happen when multiple processes inspect; use --inspect=0 for a random port. Child processes are not inspected by default, so you need NODE_OPTIONS to propagate the inspector. Finally, binding to 0.0.0.0 exposes arbitrary code execution, so always stick to 127.0.0.1 unless you are on an isolated network.
What pairs with this
This skill is part of the software development category in Hermes Agent. It pairs naturally with the systematic-debugging skill for a structured approach to finding bugs, and with python-debugpy for debugging Python processes. Together, they cover the full debugging workflow across both Node.js and Python in the Hermes environment.