Find and Replace Across All Your Markdown Notes
To rename one thing across every Markdown note at once, do three steps in order: preview the change with rg "old" --replace "new" (which never touches your files), back up with sed -i.bak, then replace in place. Because your notes are plain text in a folder, the whole text toolchain is already a bulk-editor you own.
You already know how to find across a plain-text vault: your filesystem plus a tool like grep or ripgrep is an index-free search engine, the subject of our companion post on searching instead of organizing. This post is the edit-side of the same fact. You renamed a project, retired a tag, fixed a convention — and now it is wrong in two hundred files. The output of a search can feed an editor, and the safety of the whole operation lives in one rule: look before you write.
How do I find and replace a word across all my Markdown files at once?
List the files that match, then run an in-place replace over exactly that list. The canonical one-liner is rg "old" --files-with-matches | xargs sed -i "s/old/new/g": ripgrep prints "the paths with at least one match" 1, and sed -i edits those files in place 2. But never run it first. Preview, then back up, then write.
The pipeline has two halves doing two different jobs. Ripgrep's --files-with-matches (-l) is the finder: it hands you the exact set of files that contain the old string, and nothing else. sed is the writer: it opens each of those files and substitutes the text. Keeping the finder and the writer as separate tools is not an inconvenience — it is the design that lets you inspect the match set before a single byte changes on disk.
Why does ripgrep refuse to edit files itself?
Because a search tool that can also modify files is one you can no longer trust to be safe. Ripgrep's own --replace documentation is blunt: it "Replaces every match with the text given when printing results. Neither this flag nor any other ripgrep flag will modify your files." 3 What you see is a preview — the files stay untouched.
That separation is a deliberate stance, not an oversight. When users asked ripgrep's author, Andrew Gallant, to add in-place replacement, he declined for a reason worth quoting in full: "It would change rg from a tool that will never modify your files to one that will modify your files. It is also a very complicated feature to get right to make sure you don't really mess up files on disk." 4 A previewer that cannot clobber your vault is more useful, not less.
Step 1: Preview the change before you write anything
Run rg "old" --replace "new" and read the output. Ripgrep prints every match rewritten the way sed would rewrite it, but writes nothing to disk 3. This is your dry run. If the preview shows the change landing somewhere you did not intend, you fix the pattern now — while a wrong pattern still costs nothing.
# preview only — ripgrep never writes; this just SHOWS the result
rg "Project Phoenix" --replace "Project Atlas" ~/notes
Read that output as if it were the diff you are about to commit. Are there matches inside words you did not mean to touch? Inside code blocks or URLs? The preview is the moment to catch a greedy pattern before it catches you. Gallant's design hands you this look at no cost; take it every single time.
Step 2: Back up in place, then replace
Once the preview looks right, make sed keep a copy of each original as it writes. GNU sed's -i "specifies that files are to be edited in-place" 2; adding a suffix turns that into a backup. So sed -i.bak edits note.md and leaves the original beside it as note.md.bak.
That suffix is the documented backup mechanic: it "is used to modify the name of the old file before renaming the temporary file, thereby making a backup copy" 5.
# back up AND replace — every edited file keeps a .bak copy of the original
rg "Project Phoenix" --files-with-matches | xargs sed -i.bak "s/Project Phoenix/Project Atlas/g"
# verified the result? remove the backups
find ~/notes -name "*.md.bak" -delete
Keep the .bak files until you have confirmed the result, then delete them. A backup you can throw away is cheap; a vault you cannot un-clobber is not. This is the non-negotiable middle of the method.
What about macOS? The GNU-versus-BSD sed -i trap
On macOS, that command errors — and the fix is one space. GNU sed (Linux) takes the suffix glued to the flag: sed -i.bak, or sed -i with no argument. BSD sed (macOS) instead requires a separate argument after -i, even an empty one: sed -i '' 's/old/new/g' for no backup, or sed -i '.bak' 's/old/new/g' to keep one.
This single difference is the most common reason a copy-pasted pipeline "doesn't work on my Mac." If you see sed: 1: "...": invalid command code, you are on BSD sed and forgot the empty-string argument.
# macOS / BSD sed — note the empty '' (no backup) or '.bak' (keep one)
rg "Project Phoenix" -l | xargs sed -i '' "s/Project Phoenix/Project Atlas/g"
The portable habit is to always pass the suffix argument explicitly. Decide whether you want a backup, write the suffix either way, and the same muscle memory works on both systems.
My note filenames have spaces — why did the pipe break?
Because a plain xargs splits on whitespace, so My Meeting Notes.md arrives as three broken arguments — and this is where naive pipelines quietly corrupt the wrong files. The fix is to delimit by NUL bytes instead. Ripgrep's -0/--null flag prints "a NUL byte after file paths" and is, in its own docs, "useful for use with xargs" 6.
Pair it with xargs -0, whose GNU documentation explains why it matters: with -0, "Input file names are terminated by a null character instead of by whitespace, and any quotes and backslash characters are not considered special (every character is taken literally)." 7 A NUL byte cannot appear inside a filename, so nothing splits where it should not.
# NUL-delimited and space-safe: -l0 on ripgrep, -0 on xargs
rg -l0 "Project Phoenix" | xargs -0 sed -i.bak "s/Project Phoenix/Project Atlas/g"
Make this your default form. The NUL-delimited pipeline costs two characters and removes an entire class of "it ate the wrong file" failures.
What if I don't have ripgrep installed?
Fall back to POSIX grep, which ships on every Unix-like machine. Its -l/--files-with-matches flag does the same finder job: it suppresses normal output and instead "print[s] the name of each input file from which output would normally have been printed." 8 Combine it with -r for a recursive walk, then pipe into sed exactly as before.
# no ripgrep? POSIX grep -rl does the same finder job
grep -rl "Project Phoenix" ~/notes | xargs sed -i.bak "s/Project Phoenix/Project Atlas/g"
The substitution half does not change — sed is sed whether the file list came from rg or grep. Ripgrep is faster and skips .git and binaries by default, but the method survives without it, on a machine you have never set up.
How do I do this in Vim, with undo?
Use the quickfix list as a reviewable, undoable version of the same pipeline. Populate it with a search, narrow it if needed, then run one substitute across every file at once. Where the CLI writes immediately, Vim stages the change in the editor first, so a wrong replace is one :undo away rather than a restore from .bak.
The full flow: :grep "Project Phoenix" **/*.md fills the quickfix list; the built-in cfilter plugin — added to Vim in patch 8.1.0311, whose changelog reads "Problem: Filtering entries in a quickfix list is not easy. Solution: Add the cfilter plugin. (Yegappan Lakshmanan)" 9 — narrows that list; then :cfdo runs your edit. Per Vim's help, :cfdo will "Execute {cmd} in each file in the quickfix list." 10
" 1. find every matching file into the quickfix list
:grep "Project Phoenix" **/*.md
" 2. (optional) narrow the list before editing
:packadd cfilter
:Cfilter /Phoenix/
" 3. replace across every file in the list, and save each
:cfdo %s/Project Phoenix/Project Atlas/g | update
The cfilter step is the reason to prefer Vim when scope matters: you can inspect and trim the match list before :cfdo touches a thing, then lean on the editor's undo if the result surprises you.
Is there a friendlier tool than sed?
Yes — fnr is built to feel like your editor's find-and-replace rather than a stream editor. Its author, erik, frames it directly: fnr "is intended to be more intuitive to use than sed, but is not a drop in replacement. Instead, it's focused on making bulk changes in a directory." 11 It is a gentler on-ramp.
Honesty matters more than convenience here, so heed the author's own warning. The README states plainly: "fnr is alpha quality. Don't use --write in situations you wouldn't be able to revert." 12 Treat that as the same preview-then-write discipline by another name. Run it without --write first, read what it would do, and only then commit — exactly as you would dry-run rg --replace before unleashing sed.
The bulk-edit toolbox, side by side
There is no single right tool — there is the one that matches your comfort and the scope of the change. The common thread is that every one of them operates on plain files in a folder, which is precisely why you get to choose. The table below is honest about which tools write and which only preview.
| Tool | What it does | Writes files? | Best for |
|---|---|---|---|
rg --replace | Search with a previewed replacement | No — never 3 | The mandatory dry run before any write |
rg -l0 + xargs -0 + sed -i.bak | The space-safe CLI pipeline | Yes (sed writes; .bak backs up) | Fast, scriptable bulk replace across a whole vault |
grep -rl + xargs sed | The no-ripgrep fallback | Yes | Machines without ripgrep installed |
Vim :cfdo %s/…/…/g | update | Quickfix bulk-edit | Yes, in-editor with undo | Reviewable, undoable, scope-limited edits |
fnr | IDE-style find/replace CLI | Yes (with --write) | Non-sed users; alpha quality 12 |
Notice what is not in this table: a proprietary note format. The lock-in shape of any closed-database app is that you get only the batch-edit its own interface happens to ship — and not one tool more. A folder of plain files inverts that: every text tool ever written is a feature your notes already have.
This is the same ownership point Obsidian's Steph Ango makes about durability — "File over app is a philosophy: if you want to create digital artifacts that last, they must be files you can control, in formats that are easy to retrieve and read" 13 — applied to operability rather than portability. Owning your files is not only being able to take them elsewhere; it is being able to change every one of them at once, with tools you already have.
The companion case for keeping notes as files you can leave with is in our post on exporting before you're forced out.
The four mistakes that clobber a vault
Most bulk-edit disasters trace to one of four well-known foot-guns: editing without a preview, editing without a backup, breaking on filenames with spaces, and a greedy pattern that hits more than you meant. Name them, and the preview-back-up-replace method above defends against each one in turn.
- No preview. Running
sed -iwithout first readingrg --replaceoutput is editing blind — and the preview costs nothing and writes nothing 3, so there is no reason to skip it. - No backup. A wrong regex with
sed -ihas no undo.sed -i.bakkeeps the original as.bak5 — delete the backups only after you have verified the result. - Filenames with spaces. A naive
xargssplitsMy Note.mdinto pieces. Always userg -l0 … | xargs -0 …so paths are NUL-delimited and taken literally 6 7. - Greedy, over-broad patterns. Replacing
catalso hitscategoryandconcatenate. Anchor the pattern with word boundaries —\bcat\b— and confirm the match set in the preview before you write.
The discipline underneath all four is one sentence: preview, back up, then replace. Skip none of those, in that order, and the worst a bad pattern can do is leave you a pile of .bak files to restore from.
Frequently Asked Questions
These are the questions people actually type when a renamed project or retired tag has gone wrong across a folder of notes. Each answer keeps the same spine the post does: preview with ripgrep, which never writes; back up with sed -i.bak; then replace in place, NUL-safely, and verify before you delete the backups.
How do I find and replace a word across all my Markdown files at once?
Preview, back up, then replace. First run rg "old" --replace "new" to see the change without writing anything — ripgrep never modifies your files 3. When the preview looks right, run rg -l0 "old" | xargs -0 sed -i.bak "s/old/new/g", which lists matching files NUL-safely and replaces in place while keeping a .bak of each original 1 5.
How do I rename a tag across my whole notes vault?
Treat the tag as the search string and run the same preview-backup-replace flow. Preview with rg "#oldtag" --replace "#newtag", confirm it only hits real tags and not substrings, then write with rg -l0 "#oldtag" | xargs -0 sed -i.bak "s/#oldtag/#newtag/g". Anchor the pattern if the tag text appears inside other words, and verify before deleting the .bak files.
Is there a safe way to find and replace in many files at once?
Yes — the safety is in the order. Ripgrep's --replace previews the exact result and "Neither this flag nor any other ripgrep flag will modify your files" 3, so you always look before writing. Then sed -i.bak makes a backup copy of each original as it edits 5. Keep the backups until you have checked the outcome; restore from them if the pattern misfired.
Why can't I just use ripgrep to do the replacement?
By design, ripgrep only previews. Its author, Andrew Gallant, kept it that way deliberately: in-place replacement "would change rg from a tool that will never modify your files to one that will modify your files," and is "a very complicated feature to get right" 4. The preview is the safety feature; sed, Vim, or fnr does the writing.
My sed command errors on macOS — why?
macOS ships BSD sed, which requires an argument after -i, unlike GNU sed on Linux. Use sed -i '' 's/old/new/g' for no backup or sed -i '.bak' 's/old/new/g' to keep one. If you see invalid command code, you are on BSD sed and missing the empty-string argument. Passing the suffix explicitly makes the same command portable across both systems.
My filenames have spaces and the pipe broke — how do I fix it?
Switch to NUL-delimited paths. A plain xargs splits on whitespace, so My Note.md breaks apart. Use ripgrep's -0 flag, which prints "a NUL byte after file paths" for exactly this purpose 6, with xargs -0, which then treats every character literally 7: rg -l0 "old" | xargs -0 sed -i.bak "s/old/new/g". A NUL byte cannot occur inside a filename, so nothing splits wrongly.
How do I do this with undo, in an editor?
Use Vim's quickfix list. Run :grep "old" **/*.md to collect matching files, optionally narrow with the cfilter plugin (:packadd cfilter then :Cfilter /pattern/), then :cfdo %s/old/new/g | update, which executes the substitute "in each file in the quickfix list" 10 and saves each. Because the edits happen in the editor, a wrong replace is one :undo away.
A search tool should find; an editor should write — and the gap between them is where you get to look before you leap. Keep your notes as plain text in a folder and that gap is yours to use, with every tool the last forty years of Unix has already built. To keep notes that way, mnmnote.com stores everything as plain Markdown on your own device, where ripgrep, sed, Vim, and any tool you trust can already read and change them.
Footnotes
-
"ripgrep
--files-with-matches/-lflag," ripgrep source (crates/core/flags/defs.rs), Andrew Gallant (BurntSushi). "Print the paths with at least one match." https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md. Accessed 2026-06-19. ↩ ↩2 -
"Command-Line Options —
-i[SUFFIX]/--in-place," GNUsedmanual. "This option specifies that files are to be edited in-place. GNU sed does this by creating a temporary file and sending output to this file rather than to the standard output." https://www.gnu.org/software/sed/manual/sed.html. Accessed 2026-06-19. ↩ ↩2 -
"ripgrep
--replace/-rflag," ripgrep source (crates/core/flags/defs.rs), Andrew Gallant (BurntSushi). "Replaces every match with the text given when printing results. Neither this flag nor any other ripgrep flag will modify your files." https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md. Accessed 2026-06-19. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Andrew Gallant (BurntSushi), comment on ripgrep issue #74, "Add support for replace in files," 2016-09-25. "It would change rg from a tool that will never modify your files to one that will modify your files. It is also a very complicated feature to get right to make sure you don't really mess up files on disk." https://github.com/BurntSushi/ripgrep/issues/74. Accessed 2026-06-19. ↩ ↩2
-
"Command-Line Options —
-i[SUFFIX]/--in-place," GNUsedmanual. The extension "is used to modify the name of the old file before renaming the temporary file, thereby making a backup copy." https://www.gnu.org/software/sed/manual/sed.html. Accessed 2026-06-19. ↩ ↩2 ↩3 ↩4 -
"ripgrep
--null/-0flag," ripgrep source (crates/core/flags/defs.rs), Andrew Gallant (BurntSushi). "Print a NUL byte after file paths." and "This option is useful for use withxargs." https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md. Accessed 2026-06-19. ↩ ↩2 ↩3 -
"
xargsoptions —-0/--null," GNU findutils manual. "Input file names are terminated by a null character instead of by whitespace, and any quotes and backslash characters are not considered special (every character is taken literally)." https://www.gnu.org/software/findutils/manual/html_node/find_html/xargs-options.html. Accessed 2026-06-19. ↩ ↩2 ↩3 -
"File and Directory Selection —
-l/--files-with-matches," GNUgrepmanual. "Suppress normal output; instead print the name of each input file from which output would normally have been printed. Scanning each input file stops upon first match." https://www.gnu.org/software/grep/manual/grep.html. Accessed 2026-06-19. ↩ -
"version8.txt — patch 8.1.0311," Vim documentation (vimhelp.org). "Problem: Filtering entries in a quickfix list is not easy. Solution: Add the cfilter plugin. (Yegappan Lakshmanan)." https://vimhelp.org/version8.txt.html. Accessed 2026-06-19. ↩
-
"
:cfdo," Vim quickfix documentation (vimhelp.org). "Execute {cmd} in each file in the quickfix list." https://vimhelp.org/quickfix.txt.html. Accessed 2026-06-19. ↩ ↩2 -
"fnr — About," fnr README, erik. "
fnris intended to be more intuitive to use thansed, but is not a drop in replacement. Instead, it's focused on making bulk changes in a directory, and is more comparable with your IDE or editor's find and replace tool." https://github.com/erik/fnr. Accessed 2026-06-19. ↩ -
"fnr — About," fnr README, erik. "fnr is alpha quality. Don't use
--writein situations you wouldn't be able to revert." https://github.com/erik/fnr. Accessed 2026-06-19. ↩ ↩2 -
Steph Ango, "File over app." "File over app is a philosophy: if you want to create digital artifacts that last, they must be files you can control, in formats that are easy to retrieve and read." https://stephango.com/file-over-app. Accessed 2026-06-19. ↩