Skip to content

Instantly share code, notes, and snippets.

@Kreijstal
Last active April 29, 2026 11:13
Show Gist options
  • Select an option

  • Save Kreijstal/7b3def1a1df0069499c2183ee951b4fe to your computer and use it in GitHub Desktop.

Select an option

Save Kreijstal/7b3def1a1df0069499c2183ee951b4fe to your computer and use it in GitHub Desktop.
Reentrant radare2: auto-resume your project per binary (no -p, no wrapper)

Reentrant radare2: auto-resume your project per binary (no -p, no wrapper)

Reentrant radare2: never lose your renames again

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:

  1. A sidecar file <binary>.r2prj next to each binary. Its contents are just the project name (one line, e.g. mybinary).
  2. A global ~/.radare2rc with a cmd.load hook. cmd.load is 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 first Ps creates 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 makes q prompt you to save.
  • prj.vc = true — every save is a git commit inside the project dir. Free undo.
  • prj.files = falseimportant. If true, radare2 bundles a copy of the binary into the project dir. But since cmd.load fires 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; Ps to 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=; before P $name — critical. The project's rc.r2 file restores bin.* eval settings (e.g. bin.baddr, bin.cache, bin.filter), which trigger radare2 to re-map the binary. Re-mapping fires cmd.load again. Without the guard, this creates an infinite loop: cmd.loadP → project restores bin.* → re-map → cmd.load → … forever.

2. The sidecar, once per binary

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.

Demo: prove it with a full round-trip

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.

1) Put struct definitions in a header

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;
};

2) Load types in r2 and bind arguments/locals

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.

Gotchas

  • rc runs before file load, cmd.load runs after. Any hook that needs $R2_FILE must live in cmd.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, or e file.path=/new/path; Ps to re-point.
  • Cutter uses a different project format (.rzdb). This setup is for the r2 CLI only.
  • prj.files = true is tempting (self-contained project) but conflicts with cmd.load because the bundled binary double-maps on top of the one you just opened. If you want bundling, use a shell wrapper that calls r2 -p projname instead of r2 binary, so the binary is never opened separately.

Infinite-loop bug: project stores cmd.load, re-mapping re-fires it

The project file (rc.r2) stores all eval settings, including cmd.load and bin.* variables. When the project is loaded:

  1. rc.r2 is executed as r2 commands.
  2. Early on, 'e cmd.load = ... restores the hook.
  3. Later, 'e bin.baddr = ..., 'e bin.cache = ..., etc. are restored.
  4. Changing bin.* settings triggers radare2 to re-map the binary.
  5. Re-mapping fires cmd.load → hook runs → tries P projectname"ERROR: There's a project already opened".
  6. But re-mapping continues for each remaining bin.* setting, re-firing cmd.load each time.

The loop is not truly infinite (it finishes when all bin.* settings are restored), but the noise and errors make the setup unusable.

Fix

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.

Tested on

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment