r2js
Execute arbitrary radare2 commands and r2js scripts via r2xsql when SQL surfaces are insufficient. Use for custom analysis passes, ESIL emulation, anything r2's tables don't expose, or to bridge SQL q
SKILL.mdskills/r2js/SKILL.md
GitHubWhen to use
Pick this skill when SQL alone can't answer the question:
- r2 commands not surfaced as tables (e.g. ESIL emulation,
aae*,agCcall-graph dump,p8raw-byte read at an address,wxto patch bytes, …) - multi-step r2 scripts where each step depends on r2 state, not just table data
- custom analyses written in r2js (
#!js) — typed JS that runs inside r2 with full SDK access - pivoting from SQL results back into r2 (e.g. drop temporary flags so the next SQL query sees them)
How r2xsql exposes raw r2 access
r2xsql holds a Backend (either LibrBackend or R2PipeBackend) and
forwards arbitrary commands through Session::raw_cmd(std::string).
From the CLI, raw r2 commands are reachable via:
| transport | syntax |
|---|---|
r2xsql -q/REPL |
a line starting with . runs a raw r2 command (the leading . is stripped), e.g. r2xsql -s a.bin -q ".pdf @ entry0" |
r2xsql --http |
POST /query body .r2cmd <command> → {"success":true,"output":"…"} |
| in-r2 plugin | the host RCore * is the r2 shell already |
| r2js inside r2 | r2cmd("aflj") returns JSON; round-trip via SQL |
| temp flags bridge | r2js writes flags, SQL reads them via flags table |
Recommended pattern: do the analysis pass in r2js, persist its results as r2 flags / comments, then query those from SQL.
Recipe — emulate a function and capture register state
// my_emulate.r2.js
const target = 0x401000;
r2cmd(`s ${target}`); // seek
r2cmd("aei; aeim; aeip"); // init ESIL, mem, ip
for (let i = 0; i < 50; i++) r2cmd("aes"); // step 50 instructions
const regs = r2cmd("arj"); // dump registers as JSON
const rax = JSON.parse(regs).rax;
r2cmd(`CC ESIL: rax=${rax.toString(16)} after 50 steps @ ${target}`);
Then SQL-side:
SELECT text FROM comments WHERE text LIKE 'ESIL:%';
Recipe — bridge query → r2js → query
SQL: find candidate addresses.
SELECT addr FROM funcs WHERE cc > 25;r2js: feed each address into a custom pass that doesn't fit SQL:
const addrs = [0x401000, 0x402000]; // from step 1 addrs.forEach(a => { r2cmd(`s ${a}`); const summary = r2cmd("agCd"); // some non-tabular output r2cmd(`f sym.complex_${a.toString(16)} 1 ${a}`); // tag it });SQL: re-query and the tags are visible:
SELECT name, addr FROM flags WHERE name LIKE 'sym.complex_%';
Raw hex byte patching → use the patches table, not r2js
r2xsql has a SQL write surface for this: INSERT INTO patches(addr, patched_bytes) VALUES(...) stages a raw hex patch, UPDATE patches SET committed = 1 WHERE addr = ... commits it (see the data skill's
patches section for the full column set and the commit/undo scoping
caveats — commit is table-wide, undo is stack-top-only). Reach for r2js
only when patches genuinely can't express what you need: an
assembled-instruction patch (wa) or a string/zero-terminated write (wz)
instead of raw hex, which patches doesn't support in its current version.
r2cmd("wa nop @ 0x401234"); // assembled patch -- not a `patches` INSERT
r2cmd("Ps"); // persist via project
Caveats
Session::raw_cmdbypasses the table layer entirely — there's no parsing, no schema, no pushdown. You're responsible for parsing whatever r2 prints.- r2js scripts run inside r2 (not inside r2xsql) — they don't see the SQL database directly. Bridge via flags/comments as above.
- Raw passthrough is available on every transport:
-q "."/REPL from the CLI,.r2cmd <command>over HTTP, and the host shell in the in-r2 plugin. The raw path bypasses the table layer (no schema, no pushdown).