CLAUDE LABJP
PRICING — September 1 was the scheduled date for the Sonnet 5 price increase, and it did not happen. The introductory $2/$10 per MTok now stands as the regular pricePARTNER — Salesforce and Anthropic announced Claudeforce, an expanded partnership. The Salesforce in Claude plugin ships with 37 prebuilt sales skills, from meeting prep to pipeline managementTRUST — Claudeforce serves Claude through Amazon Bedrock inside the Salesforce Trust Boundary, so data and inference never leave the security perimeter — an answer aimed squarely at regulated industriesBETA — Salesforce in Claude is with select pilot customers for now, with an open beta expected during SeptemberLIMITS — The 50% weekly-limit boost runs through September 13. From September 14 the permanent level is 25% above the pre-promotion baseline, roughly a 17% cut from todayRELEASE — Claude Code has shipped nothing since v2.1.251 on August 28. Against a pace of one release every 0.8 days, a four-day gap is among the longest yetPRICING — September 1 was the scheduled date for the Sonnet 5 price increase, and it did not happen. The introductory $2/$10 per MTok now stands as the regular pricePARTNER — Salesforce and Anthropic announced Claudeforce, an expanded partnership. The Salesforce in Claude plugin ships with 37 prebuilt sales skills, from meeting prep to pipeline managementTRUST — Claudeforce serves Claude through Amazon Bedrock inside the Salesforce Trust Boundary, so data and inference never leave the security perimeter — an answer aimed squarely at regulated industriesBETA — Salesforce in Claude is with select pilot customers for now, with an open beta expected during SeptemberLIMITS — The 50% weekly-limit boost runs through September 13. From September 14 the permanent level is 25% above the pre-promotion baseline, roughly a 17% cut from todayRELEASE — Claude Code has shipped nothing since v2.1.251 on August 28. Against a pace of one release every 0.8 days, a four-day gap is among the longest yet
Articles/Claude Code
Claude Code/2026-07-14Intermediate

An Empty Variable and rm -rf: How Claude Code's Auto Mode Preflight Saved My Late-Night Cleanup

An empty variable nearly turned rm -rf into a wide delete. Why set -u lets an empty string pass, and the cleanup script I rebuilt with dry run as the default.

Claude Code241auto mode3indie developer20operations25safety4

Late at night, I handed Claude Code the chore of clearing the build caches that had piled up across several of my app repositories.

DerivedData, CocoaPods' Pods, the React Native node_modules, .build. When you carry several apps in parallel as an indie developer, these intermediate artifacts quietly eat away at your disk.

Deleting them by hand, one repository at a time, is dull work. That is exactly why it seemed right for an agent.

Partway through, the terminal paused for a moment. The command about to run looked like this.

rm -rf "${CACHE_ROOT}"/*

CACHE_ROOT was empty. Expanded, that command becomes rm -rf /*. My hands went a little cold.

Why the variable came up empty

The cause was ordinary.

The cleanup script was designed to place each repository's cache location into an environment variable before running. But one of the targets was an app whose directory layout I had reorganized only recently.

The path I expected no longer existed, so nothing was assigned when CACHE_ROOT was built. The shell silently treats an undefined variable as an empty string. No error, no warning.

The intent behind rm -rf "${CACHE_ROOT}"/* was "delete only this app's cache." Yet the moment it collapsed into an empty string, the meaning became something else entirely.

In solo development, I am the only tester and the only reviewer. There is usually no second pair of eyes to catch a mix-up like this.

An empty string slips past set -u: four combinations I checked by hand

One thing kept nagging at me afterward.

My cleanup script started with set -euo pipefail. The -u is supposed to stop the shell the moment an undefined variable is used. So why did an empty CACHE_ROOT sail straight through?

I went and checked on a clean bash 5.1.16. First, how far does an empty variable actually reach? Rather than delete anything, I counted the expansion as arguments.

# Nothing is deleted here; we only count what the glob expands to
unset CACHE_ROOT
set -- "${CACHE_ROOT}"/*
echo "argument count: $#"
echo "first three: $1 $2 $3"

The output on my machine:

argument count: 25
first three: /bin /boot /dev

Twenty-five is the number of entries directly under root. The incident I had been describing vaguely as "nearly deleted something wide" now had a shape: twenty-five starting points.

Then I ran every combination of variable state and defense.

Variable stateDefenseResult
Undefined (unset)set -uStops (bash: unbound variable)
Undefined (unset)${VAR:?}Stops
Empty string assigned (VAR="")set -uDoes not stop; passes through
Empty string assigned (VAR="")${VAR:?}Stops

The third row was my answer.

What set -u watches is whether the variable is defined, not whether it holds anything. When my script failed to assemble the path, it did not leave the variable unset. It assigned an empty string. Defined, therefore silent.

${VAR:?message}, on the other hand, rejects both the undefined and the empty case. One line placed just before you use the value closes the gap.

: "${CACHE_ROOT:?CACHE_ROOT is empty}"

One caveat. The exit status of that stop differs by shell. Under identical conditions bash returned 127 and dash returned 2. If you branch on a specific number, your check quietly stops working the moment the runtime changes. Test for non-zero and leave it at that.

Auto mode was watching for "variables it could not resolve from context"

What stopped it was Claude Code's auto mode.

In the July 2026 update, auto mode added several safe-by-default behaviors. One of them is that an rm -rf containing a variable it cannot resolve from context now pauses for confirmation before running.

The agent could not tie CACHE_ROOT to a reliable value. So instead of running silently, it handed the decision back to me.

What matters here, I think, is that this is not a blunt "stop every destructive command." That would produce so many prompts that you would end up approving without reading.

What it watches for is whether the value can be resolved from context. It surfaces only the deletions it cannot back with a real value. That line felt like a kindness toward the attention of the person doing the delegating.

Auto mode also now blocks tampering with the session record itself. The trace of what was executed cannot be rewritten afterward, and the more unattended the work, the more that line earns its place.

What it felt like to hand a long cleanup to Opus 4.8

That same day, Claude Opus 4.8 became generally available. It is a model with a lift in coding, agentic work, and consistency across long sequences of steps.

This cleanup was a quietly long job whose steps simply repeated for each repository. Check the cache location, judge what is safe to remove, remove it. Having it carry that back-and-forth to the end while holding context was, plainly, a relief.

That said, I did not leave it fully on its own.

I asked it to always print the list of deletion targets as text before executing. That is a step I added to the instructions. The smarter the model gets, the more it pays off to write down my own "shape of confirmation."

Here is the target check before deletion, in pseudocode.

# Always visualize targets and approve before deleting
for repo in "${REPOS[@]}"; do
  cache="${repo}/build-cache"
  # Never pass a path we cannot back with a value into the delete loop
  if [ -z "${cache}" ] || [ ! -d "${cache}" ]; then
    echo "skip: ${repo} (cache unconfirmed)"
    continue
  fi
  echo "delete candidate: ${cache}"
done

That single line, [ -z "${cache}" ], is the wall that turns away the empty variable of that night. Auto mode's preflight, plus my own script-side defense. With both in place, I can stay calm even while delegating overnight.

The confirmation rules I settled into

After this incident, I put a few promises into words for whenever I hand a cleanup-type job to an agent.

SituationWhat I decided
Just before deletionAlways print the absolute paths of targets as text, and approve only after reading them
Handling variablesAny variable that builds a path must be empty-checked; if empty, skip that target
PermissionsNever disable auto mode's preflight for steps that include rm -rf
RecordsKeep execution logs on the assumption they cannot be altered, so the next morning I can trace what was removed

None of this is special. But on the ground of solo development, whether you can hold these obvious things as a system is what decides whether you can let go with peace of mind.

Unlike AdMob settings or an App Store build, cache cleanup is dull work whose results are hard to see. That is precisely why it turns sloppy, and why accidents happen there.

Making dry run the default: the rewritten script and what it actually printed

Writing rules down is one thing. The person who runs the script is a tired version of me at midnight. Unless the design falls toward the safe side even when unread, I will probably repeat myself.

So I rewrote the cleanup script. Dry run is the default; deleting for real requires saying so explicitly.

#!/usr/bin/env bash
set -Eeuo pipefail
 
DRY_RUN="${DRY_RUN:-1}"          # Dry run by default; pass DRY_RUN=0 to delete
ROOT="${1:?ROOT is required}"    # Both undefined and empty fail right here
 
total_kb=0
while IFS= read -r cache; do
  : "${cache:?cache path is empty}"   # Re-check inside the loop, every time
  case "$cache" in
    /|/*/) echo "refuse: $cache (root-level path)" >&2; continue ;;
  esac
  kb=$(du -sk "$cache" | cut -f1)
  total_kb=$((total_kb + kb))
  printf '%-28s %8s KB\n' "$cache" "$kb"
  [ "$DRY_RUN" = "0" ] && rm -rf -- "$cache"
done < <(find "$ROOT" -maxdepth 2 -type d -name build-cache | sort)
 
echo "----"
echo "total ${total_kb} KB / DRY_RUN=${DRY_RUN}"

To test it I created two repositories holding caches and one whose cache had disappeared after a layout change. The default dry run prints this.

/tmp/guardtest/demo/repoA/build-cache    12292 KB
/tmp/guardtest/demo/repoC/build-cache     7172 KB
----
total 19464 KB / DRY_RUN=1

repoB, which has no cache, never appears in the list. Before anything is removed, what is targeted and how much it adds up to are both visible as numbers.

I also tried forgetting the argument, and passing an empty string. Both failed at the same place, with exit status 1.

./clean.sh: line 5: 1: ROOT is required

Running it again with DRY_RUN=0 removed exactly the two entries that had been listed, and left repoB's directory untouched. What the dry run showed and what actually happened matched. Being able to confirm that match with my own eyes is, for me, what peace of mind is made of.

One pitfall I hit while writing it. I first piped find ... | while read, and the total stayed at zero all the way through.

piped version:              total=0
process substitution:       total=19464

The right side of a pipe runs in its own shell, so the total_kb accumulated inside the loop never comes back out. Switching to process substitution with done < <(find ...) produced the expected sum. For any script that reports a total, this fails quietly, which is exactly the kind of failure worth watching for.

Addendum, August 2026: what to check before it becomes the default

After I wrote this piece, auto mode moved past its trial standing: it is scheduled to be on by default for Pro, Max, and Team on August 14, 2026. Steps I used to halt by hand will now flow by default.

In explaining the change, Anthropic noted that 97% of Claude Code's permission prompts are approved. It is the company's own tally, so the figure deserves careful handling, but that particular point landed. The version of me before that night was surely approving prompts without reading them too.

A confirmation you approve without reading is not really a defense. Seen that way, the switch is less a thinning of protection than a replacement of protection that was not working with something more honest.

Before the default flipped, I checked three things on my own machine.

What I checkedHow
Where the line falls between stopping and proceedingRun deletions, force pushes, and outbound writes in a throwaway repository and record the outcomes
Risky steps specific to my own setupList the steps that proceeded when I wanted them stopped, and write them out as deny rules
Whether my script-side defenses are aliveExtend the dry run and ${VAR:?} from this article to procedures beyond cleanup

The third mattered most to me. What auto mode stops is an operation whose value it cannot substantiate. A mistake where the value is perfectly substantiated but simply is not what I meant looks, from the agent's side, like a correct operation. That gap is mine to close.

What I plan to work on next

I am extending the dry run to procedures beyond cleanup. Migration scripts, log tidying, anything where the list can be printed first fits the same shape.

The open question is granularity in the deny rules. Written broadly they stop too often and you stop reading; written narrowly they leak. For a while I plan to record how often things actually stopped, and how many of those were genuinely close calls, and tune from my own numbers.

The range you can hand to a capable agent has certainly grown. At the same time, the responsibility of the one delegating to decide in advance "where I want you to stop" is, quietly, growing too.

I do not think I will forget those cold hands for a while. I am simply grateful for the preflight that took a breath before deleting. If it helps even a little as a small safeguard for someone who also entrusts work to the night, I would be glad. Thank you for reading.

Share

Thank You for Reading

Claude 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 $15 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

Claude Code2026-07-19
I Could No Longer Remember What I'd Changed in Auto Mode — Where claude auto-mode reset Fits In
Running nightly automation for weeks, I kept nudging my auto-mode settings one at a time. One morning a small oddity made me realize I could not recall what I had changed. Here is how I rebuilt my configuration around claude auto-mode reset as a known-good baseline, from a solo developer's field notes.
Claude Code2026-08-31
Half of My Scheduled Runs Vanished Without a Single Error
A batch job set to run twice a day was only firing once. No errors, no failure alerts. Here is how to expand your own schedule, count expected runs, and reconcile them against execution records to catch silent misses.
Claude Code2026-08-27
Curating the /model picker with modelPicker, and what replacing the lineup hides
Claude Code v2.1.242 added modelPicker, which lets you write the /model lineup yourself. Here is how appending differs from replacing, why project settings are ignored, and where it quietly narrows what availableModels allows.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →