◉ANTIGRAVITY LABJP
Articles/Tips & Best Practices
✦ Tips & Best Practices/2026-10-30Beginner

The sed -i That Worked on Linux and Stalled on My iMac: Checking an Agent's Steps Before You Run Them

A sed -i step that ran fine on Linux can stall on a Mac or leave backup files behind. Here is what I reproduced with GNU sed, the forms that behave the same on both, and a short check to run first.

sedmacOSshell scriptingagent operations10indie development27Antigravity406

One evening I asked an agent to draft a procedure for rewriting a handful of config values in bulk. It ran cleanly in a Linux environment, so I pasted the same steps into the terminal on my iMac at home.

Nothing was replaced. The terminal printed a single error I didn't recognize, and that was it. The cause was a small difference in how sed -i is written.

As an indie developer, I move between a Linux server and my iMac all the time, so steps that cross that line are routine. Procedures that come back from an agent often lean on Linux conventions. Before you run one on your own Mac, here is what I look at, along with a small reproduction.

Where "it worked" actually lives

That evening I treated "it ran on Linux" as proof the procedure was correct. What had actually run was the procedure together with that particular environment.

sed is the classic case of one name with two behaviors. The sed that ships with macOS and the GNU sed common on Linux disagree about what follows -i.

Reproducing the difference

I ran this on a Linux machine with GNU sed 4.8. I did not test macOS directly, so the macOS column below reflects the documented behavior rather than my own measurement.

Take a small config file and replace old with new.

printf 'API_HOST=old.example.com\nMODE=old\n' > app.env
sed -i 's/old/new/' app.env
cat app.env

With GNU sed, both lines change to new, and no extra file appears.

Next I tried the form usually recommended for macOS, an empty string right after -i, on the same Linux machine.

sed -i '' 's/old/new/' app.env

The result:

sed: can't read s/old/new/: No such file or directory

Exit code 2, and the file still said old. GNU sed read '' as an empty script and then tried to open s/old/new/ as a filename.

So the argument after -i means different things in different places. A Linux-style short form stalls on a Mac, and a Mac-style form stalls on Linux.

FormGNU sed (measured here)macOS sed (documented behavior)
sed -i 's/old/new/' fReplaces (exit 0)Reads the expression as the backup suffix and then misreads the next argument, so it errors
sed -i '' 's/old/new/' fFails (exit 2, file unchanged)Replaces
sed -i.bak 's/old/new/' fReplaces and leaves f.bakReplaces and leaves f.bak

Only the third row means the same thing on both. That is the way out.

Three forms that behave the same

The first is -i.bak, with the suffix attached directly. GNU sed replaces in place and keeps f.bak, and macOS does the same.

The second is perl -pi -e. On the same config file it replaced the values and left no stray files.

perl -pi -e 's/old/new/' app.env

Perl ships with macOS, which makes it convenient for procedures you want to reuse. Regular expression details differ slightly from sed, so test anything complicated before trusting it.

The third is to detect which sed you have before using it.

if sed --version >/dev/null 2>&1; then
  SED_INPLACE=(sed -i)        # GNU sed
else
  SED_INPLACE=(sed -i '')     # sed bundled with macOS
fi
"${SED_INPLACE[@]}" 's/old/new/' app.env

GNU sed answers --version; the macOS one does not. Because the script checks first, it no longer depends on which environment the author had in mind.

If you keep backups, clean up in the same step

-i.bak is safe, but it leaves files behind. Running it over three files turned a directory of 5 entries into 8, and a cleanup rm brought it back to 5.

In a Git repository the leftovers showed up as untracked files next to the modified ones.

 M g1.txt
 M g2.txt
 M g3.txt
?? g1.txt.bak
?? g2.txt.bak
?? g3.txt.bak

I've nearly done exactly that in my own site's repository; I noticed just before the commit. A careless git add . would commit the backups too. I now put the cleanup on the same line as the replacement.

sed -i.bak 's/old/new/' g*.txt && rm -f g*.txt.bak

One more trap people mention on the macOS side: writing -i -e makes -e the backup suffix, which leaves files named like file-e. GNU sed produced no extra files for the same form. Since the outcome differs by environment, I avoid the -i -e shape altogether.

Tell the agent where the steps will run

Doing all of this checking in my head every time is tiring, so I added one line about the environment to my requests.

These steps will run on both macOS (my iMac) and Linux.
Use the -i.bak form of sed -i so it means the same thing on both,
and add a line that removes the backup files right after the replacement.

That alone makes the returned steps noticeably more consistent. Even so, I try the replacement on a single file before running it across everything.

What worked was never the procedure alone; it was the procedure plus its environment. I try to remember that sentence whenever I receive a set of steps.

The next time an agent hands you a sed -i, look at what follows -i first. If it isn't -i.bak or a perl -pi, try it on one file before you let it loose on the rest.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

✦ Tips2026-10-19
After I Rewrote the URL, Only the Not-Yet-Loaded Images Returned 404: Checking replaceState and Relative Paths
After rewriting the URL with history.replaceState when moving from a list to a detail view, only the images that had not loaded yet returned 404. Native loading=lazy broke too. Here are a minimal test page, a check script, and three fixes compared.
✦ Tips2026-10-29
The Agent Said It Used My Skill, but There Is No Trace — Check Files, Not Replies
When an agent ignores your Rules or says it used a Skill without doing so, its reply can't settle the question. Here is a small file-based check, and a way to decide between rewriting, adding a mechanical check, or handing the task elsewhere.
✦ Tips2026-10-27
A 60-Line PreToolUse Hook That Stops an Agent's rm -rf Before It Runs
How to block rm -rf and git push --force before they execute, using an Antigravity CLI PreToolUse hook. Includes what a one-line grep missed across 16 test commands.