Reentrant radare2: auto-resume your project per binary (no -p, no wrapper)
By default, radare2 forgets everything the moment you q. Every rename, comment, and flag evaporates. This is the 5-minute setup that makes r2 remember per-binary: you just run r2 /path/to/binary and your previous session comes back automatically — no -p projectname to remember, no wrapper function to type.
- Open
r2 ./foo.exe→ previous analysis, renames, comments, flags all restored. - Quit with
q→ prompt "save project?" so you can't lose work by accident. - Project history is git-tracked, so you can roll back RE mistakes.
- Works with vanilla
r2— no shell alias, no wrapper script.
Two pieces:
- A sidecar file
<binary>.r2prjnext to each binary. Its contents are just the project name (one line, e.g.mybinary). - A global
~/.radare2rcwith acmd.loadhook.cmd.loadis the radare2 config variable whose value is a command string that runs after the binary is loaded (unlike the rc body itself, which runs before load). The hook reads the sidecar, checks whether a project of that name already exists, and either loads it (P) or seeds the name (e prj.name=) so the firstPscreates it.
That's it. No plugins.
e cmd.load = .!sh -c 'f="${R2_FILE}.r2prj"; [ -f "$f" ] || exit 0; name=$(tr -d "[:space:]" < "$f"); [ -z "$name" ] && exit 0; dir="${R2_RDATAHOME:-$HOME/.local/share}/radare2/projects/$name"; if [ -d "$dir" ]; then echo "e cmd.load=;P $name"; else echo "e prj.name=$name"; fi'
e prj.alwasyprompt = true
e prj.vc = true
e prj.files = false
e prj.history = true
e scr.prompt.prj = true
Notes:
prj.alwasyprompt = true— yes, the radare2 config variable really is spelled "alwasy" (typo upstream). It makesqprompt you to save.prj.vc = true— every save is a git commit inside the project dir. Free undo.prj.files = false— important. Iftrue, radare2 bundles a copy of the binary into the project dir. But sincecmd.loadfires after the binary is already mapped, the bundled copy then tries to map on top and you get "Cannot add map" errors. Turning bundling off avoids that entirely; the price is that if you move the binary,e file.path=/new/path; Psto re-point.prj.history = true— per-project r2 command history (up-arrow remembers what you did last session).scr.prompt.prj = true— shows the project name in the r2 prompt so you can see it's active.e cmd.load=;beforeP $name— critical. The project'src.r2file restoresbin.*eval settings (e.g.bin.baddr,bin.cache,bin.filter), which trigger radare2 to re-map the binary. Re-mapping firescmd.loadagain. Without the guard, this creates an infinite loop:cmd.load→P→ project restoresbin.*→ re-map →cmd.load→ … forever.
echo mybinary > /path/to/mybinary.exe.r2prj
Any name works — it just has to be unique within ~/.local/share/radare2/projects/.
$ r2 /path/to/mybinary.exe
First open: the sidecar seeds prj.name=mybinary. Do your analysis, renames, comments. Before quitting, run Ps once to create the project. From then on, q will prompt you to save.
Subsequent opens: the sidecar is detected, the project directory exists, cmd.load runs P mybinary, and everything is back.
Using a real-world case — finding the Wine-detection check in a Delphi binary. Pipeline: open, analyze one function, rename it, save, close, reopen, confirm the rename survived.
echo mybinary > mybinary.exe.r2prj
# Session 1 — create project, do work, save
r2 -q mybinary.exe <<'EOF'
af @ 0x00423010
afn detect_wine @ 0x00423010
CCu "Returns non-NULL if running under Wine" @ 0x00423010
f wine_str 16 @ 0x00423050
Ps
q
EOF
# Session 2 — fresh invocation, expect everything to come back
r2 -q -c 'afl~detect_wine; CC. @ 0x00423010; f~wine_str' mybinary.exe
# output:
# 0x00423010 ... detect_wine
# "Returns non-NULL if running under Wine"
# 0x00423050 16 wine_str
The rename, comment, and flag are all back in session 2 without touching -p or any wrapper.
You can make pdg decomp print your own structs instead of anonymous pointer math.
Example document.h:
struct Document {
char title[0x40];
char author[0x20];
unsigned char paragraph_count;
char paragraphs[8][0x100];
unsigned char pad[0x7];
void *renderer;
};
Use to to load the header, then annotate each function argument with afvt.
e bin.relocs.apply=true
to /tmp/document.h
af @ 0x0000138a # new_document
s 0x0000138a
afva
afvt arg1 Document*
pdg
af @ 0x00001419 # add_paragraph
s 0x00001419
afva
afvt arg1 Document*
pdg
After that, decomp output uses Document * and resolves fields.
If arg1 is not the argument name, use the real variable name from
afvr in that function (for example param_1).
These type links are stored by the project and survive Ps saves like names,
comments, and flags.
- rc runs before file load,
cmd.loadruns after. Any hook that needs$R2_FILEmust live incmd.load, not the rc body. Easy mistake. - Project names are bare strings, no paths. Two binaries with the same basename need distinct sidecar contents, or they'll collide in
~/.local/share/radare2/projects/. - If you move the binary with
prj.files = false, next open will fail to find it at the recorded path. Either restore the binary, ore file.path=/new/path; Psto re-point. - Cutter uses a different project format (
.rzdb). This setup is for the r2 CLI only. prj.files = trueis tempting (self-contained project) but conflicts withcmd.loadbecause the bundled binary double-maps on top of the one you just opened. If you want bundling, use a shell wrapper that callsr2 -p projnameinstead ofr2 binary, so the binary is never opened separately.
The project file (rc.r2) stores all eval settings, including cmd.load and bin.* variables. When the project is loaded:
rc.r2is executed as r2 commands.- Early on,
'e cmd.load = ...restores the hook. - Later,
'e bin.baddr = ...,'e bin.cache = ..., etc. are restored. - Changing
bin.*settings triggers radare2 to re-map the binary. - Re-mapping fires
cmd.load→ hook runs → triesP projectname→ "ERROR: There's a project already opened". - But re-mapping continues for each remaining
bin.*setting, re-firingcmd.loadeach time.
The loop is not truly infinite (it finishes when all bin.* settings are restored), but the noise and errors make the setup unusable.
Two changes:
1. Guard in the hook — clear cmd.load before loading the project, so re-mapping can't re-fire it:
# before: echo "P $name"
# after: echo "e cmd.load=;P $name"
2. Clean the project file — after applying the fix, re-save the project. This removes the stored cmd.load line from rc.r2, preventing the problem on future loads even when cmd.load is set at startup:
r2 binary # loads project via fixed hook (see #1)
Ps # re-saves without cmd.load in rc.r2
With both changes, the project loads cleanly with no intermediate errors.
radare2 6.1.2 (abi:77)
Variable names may differ on older/newer versions (prj.autosave exists in some builds; prj.alwasyprompt on this one). Check e?? + grep prj. on your install.