Batch-renaming images sounds like a five-line script—until a destination already exists, a rename fails halfway through, or you need to recover the original filenames.
I ran into that design problem while building Creator Toolkit CLI, an open-source Python CLI for small creator workflows. The interesting part was not generating image_1.jpg; it was making the operation inspectable before it ran and recoverable afterward.
This article walks through the safety model shipped in Creator Toolkit CLI v0.1.0: deterministic planning, dry runs, collision checks, two-phase renaming, rollback, and undo manifests.
Why the obvious loop is risky
A first implementation might look like this:
from pathlib import Path
for index, source in enumerate(Path("images").glob("*.jpg"), start=1):
source.rename(source.with_name(f"image_{index}.jpg"))
It is short, but it leaves several questions unanswered:
- Is the input order stable across systems?
- What happens if
image_1.jpgalready exists? - What if that file is also one of the sources that should move?
- What happens when the third rename fails after two succeeded?
- Can a user preview the exact plan?
- Can the operation be reversed later?
For file mutations, those are not edge cases. They are the design.
1. Separate planning from mutation
The first rule is simple: build the complete plan without changing the directory.
Creator Toolkit selects supported files, sorts them case-insensitively, and returns explicit (source, destination) operations:
SUPPORTED_EXTENSIONS = {".jpeg", ".jpg", ".png"}
def plan_image_renames(folder: Path) -> list[tuple[Path, Path]]:
images = sorted(
(
item
for item in folder.iterdir()
if item.is_file() and item.suffix.lower() in SUPPORTED_EXTENSIONS
),
key=lambda item: item.name.casefold(),
)
operations = [
(source, folder / f"image_{index}{source.suffix.lower()}")
for index, source in enumerate(images, start=1)
]
return [(source, destination) for source, destination in operations if source != destination]
Planning separately gives the CLI one source of truth for preview, confirmation, JSON output, application, and tests.
Users can inspect that plan without touching a file:
creator-toolkit rename ./images --dry-run
Example output:
Planned 2 image(s).
cover.jpg -> image_1.jpg
thumbnail.png -> image_2.png
A dry run is much more useful when it exercises the same planning path as the real operation. A separate “preview approximation” can drift from the code that eventually mutates the filesystem.
2. Validate the whole plan before the first rename
Before applying anything, the implementation checks that:
- every source is unique and still exists;
- every destination is unique;
- each operation stays inside the same directory; and
- an existing destination is either another source in the plan or a collision that must stop the operation.
That last distinction matters. Consider this directory:
cover.jpg
image_1.jpg
The desired plan may be:
cover.jpg -> image_1.jpg
image_1.jpg -> image_2.jpg
image_1.jpg exists, but it is also scheduled to move. Rejecting every existing destination would incorrectly reject a valid plan. Overwriting it would be worse.
Complete-plan validation lets us distinguish a movable destination from an unrelated file that must never be overwritten.
3. Use a two-phase rename
Even a valid plan can fail if it is applied directly in source order. Creator Toolkit therefore uses two phases:
- Move every source to a unique temporary name in the same directory.
- Move each temporary file to its final destination.
Conceptually:
cover.jpg -> .creator-toolkit-stage-<uuid>.tmp
image_1.jpg -> .creator-toolkit-stage-<uuid>.tmp
.creator-toolkit-stage-<uuid>.tmp -> image_1.jpg
.creator-toolkit-stage-<uuid>.tmp -> image_2.jpg
Once phase one completes, all final destination names are free. This avoids order-dependent collisions without relying on platform-specific overwrite behavior.
The temporary files remain in the target directory so each move stays on the same filesystem. That avoids cross-device rename failures and keeps the operation local to the directory being changed.
4. Roll back both failure points
There are two different failure windows, so they need two recovery paths.
If staging fails, files that already reached temporary names are restored to their original names in reverse order.
If finalization fails, the state is mixed: some files have final names, while others still have temporary names. The implementation stages that mixed set again and then restores every original source name.
The important principle is broader than this project:
A multi-step filesystem mutation should define recovery for every intermediate state it can create.
Rollback is still best effort. Filesystems and external processes can fail in ways an application cannot fully repair. The CLI reports a distinct runtime error if restoration also fails, and the documentation still recommends a separate backup for valuable assets.
5. Write recovery metadata before applying
Interactive confirmation protects the current command, but it does not help tomorrow. Applied CLI renames therefore create a unique JSON manifest in the image directory.
A simplified manifest looks like this:
{
"version": 1,
"status": "applied",
"created_at": "2026-08-07T12:00:00+00:00",
"directory": "/absolute/path/to/images",
"operations": [
{
"source": "cover.jpg",
"destination": "image_1.jpg"
},
{
"source": "thumbnail.png",
"destination": "image_2.png"
}
],
"applied_at": "2026-08-07T12:00:01+00:00"
}
The manifest is first written with status: "pending", then changed to applied or failed. Manifest writes themselves use a temporary file followed by a replace operation, so an interrupted write is less likely to leave truncated JSON at the final path.
To preview a restore:
creator-toolkit undo ./images/.creator-toolkit-renames-<timestamp>-<id>.json --dry-run
To apply it:
creator-toolkit undo ./images/.creator-toolkit-renames-<timestamp>-<id>.json
Undo reverses the concrete operations stored in the manifest. It does not try to reconstruct intent from the current directory.
6. Treat the manifest as untrusted input
A recovery file can become dangerous if the program blindly trusts paths inside it.
Before undoing, Creator Toolkit checks the manifest version and status, requires an absolute target directory, and accepts only plain filenames for each operation. Values such as ../outside.txt are rejected because Path(value).name != value.
The reversed plan then passes through the same collision validation as a normal rename. If a new file now occupies an original filename, undo stops instead of overwriting it.
That makes the manifest useful recovery metadata, not a general-purpose instruction file for moving arbitrary paths.
7. Make automation explicit
Human-readable previews are useful at a terminal. Scripts need a stable contract.
Every scriptable subcommand supports JSON output:
creator-toolkit rename ./images --dry-run --json
Successful responses use a versioned envelope, while validation and runtime errors go to standard error as structured JSON. Exit codes distinguish success, runtime failure, and unsafe CLI usage.
JSON mode also never prompts. A mutating rename or undo must include either --dry-run or --yes; otherwise the command exits with a usage error. Automation should never hang while waiting for an interactive answer it cannot provide.
Try the released version
Creator Toolkit CLI v0.1.0 requires Python 3.11 or newer:
python -m pip install creator-toolkit-cli==0.1.0
Start with a disposable directory and a preview:
creator-toolkit rename ./images --dry-run
Then review the plan before applying it:
creator-toolkit rename ./images
The source, tests, JSON contract, and release artifacts are available here:
This article intentionally targets the released v0.1.0 tag. The main branch may contain unreleased changes.
Closing thought
The core algorithm is not large. The reliability comes from the boundaries around it: plan first, validate globally, isolate intermediate states, define rollback, persist recovery metadata, and make automation behavior explicit.
If you have handled batch file mutations differently, I would be interested in the failure cases and recovery strategies that shaped your design.

Top comments (0)