Gemini CLI's mkdir Failed, the Agent Kept Going, and Each move Overwrote the Last File
Book: LLM Observability Pocket Guide Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools Me: xgabriel.com | GitHub The agent's last me

Posted on Nov 2 • Edited on Nov 2 • AI-assisted
The agent's last message was an apology: "I have failed you completely and catastrophically." That line comes from a Gemini CLI session in July 2025, reported by Ars Technica. A product manager had asked it to rename a folder of experiments and move the files into a new one. When the session ended, one file was left.
Nothing in the chain was exotic. A mkdir failed, the agent didn't check, and a plain move did what move has always done when the destination isn't a folder. You can reproduce all of it in a scratch directory in a minute, and you should, because the same thing happens with mv on Linux and macOS.
The user filed issue #4586 on July 21, 2025: Gemini CLI 0.1.13, model gemini-2.5-pro, no sandbox, on Windows. The full transcript was an attachment, so the issue page itself doesn't show the commands. Ars filled in the sequence. The user asked Gemini to rename the current directory, claude-code-experiments, to AI CLI experiments and move its contents to a new folder. Gemini said, correctly, that it couldn't rename the directory it was running in. It then ran a mkdir for a new folder one level up (..\ ). That command apparently failed. Gemini treated it as a success and started moving files. The transcript attached to the issue shows that mkdir and then one move per item, folders included. The overwrite chain below is Ars's reconstruction of what those moves did.
Ars explains the rest. When the destination doesn't exist, Windows "renames the file to the destination name instead of moving it." The first move turned file one into a file named after the missing folder. Every later move renamed the next file to that same name, over the top of the one before. Ars quotes the user's diagnosis: Gemini "hallucinated a state," and "the core failure is the absence of a 'read-after-write' verification step." The user's own write-up, linked from the Ars piece, now returns a 404. The GitHub issue was closed as "not planned" on May 13, 2026, after a triage bot flagged it.
One detail matters for the reproduction. At the 0.1.13 tag, Gemini CLI's shell tool docs say that on Windows "the command will be executed with cmd.exe /c." So the commands ran in cmd, whatever terminal the user started Gemini from.
Three moves, one destination name that was never a folder: each file lands on the same spot.
This script touches only two names under %TEMP%. Save it as repro.cmd and run it from a Command Prompt, not from inside a project.
@echo off rem repro.cmd: touches only %TEMP%\mvdemo and %TEMP%\mvout cd /d " %TEMP% " || exit /b 1 mkdir mvdemo || exit /b 1 cd mvdemo echo alpha > a.txt echo bravo > b.txt echo charlie > c.txt rem The mkdir for..\mvout "failed". Nobody checked. move a.txt..\mvout move b.txt..\mvout move c.txt..\mvout dir /b type..\mvout Enter fullscreen mode Exit fullscreen mode I haven't run this myself, so here is what the documentation says you should see. The first move finds no folder named mvout, so it renames a.txt to a file called mvout in %TEMP%.
The second and third find that file and replace it. Microsoft's move reference says the overwrite prompt is the default "unless the command is run from within a batch script," and /y or the COPYCMD variable switches it off anywhere. A.cmd file is a batch script, so nothing asks. dir /b finds nothing left in mvdemo, and type prints charlie.
Clean up with del "%TEMP%\mvout" and rmdir "%TEMP%\mvdemo".
Gemini CLI's later releases ran Windows commands through powershell.exe -NoProfile -Command, according to its shell docs. (Its docs also say Google replaced Gemini CLI with Antigravity CLI on June 18, 2026.) PowerShell's move is an alias for Move-Item, and its reference page describes this exact trap: when the destination folder is missing, "Move-Item moves the first item to a file called Textfiles and then displays an error explaining that the file already exists." That is better, since only the first file is renamed and the rest stay put. It is still one renamed file, plus an error an agent might not read. With -Force, the overwrite is allowed again.
POSIX mv has two forms. The spec says the rename form "is assumed when the final operand does not name an existing directory." It only prompts before replacing a file if you pass -i, or if the destination isn't writable and standard input is a terminal. An agent's shell usually meets neither condition.
demo = $( mktemp -d ) cd " $demo " && mkdir src && cd src printf 'alpha\n' > a.txt printf 'bravo\n' > b.txt printf 'charlie\n' > c.txt #../out was never created for f in a.txt b.txt c.txt; do mv " $f "../out; done ls -A # expect: nothing cat../out # expect: charlie Enter fullscreen mode Exit fullscreen mode Put all three names in one command, mv a.txt b.txt c.txt../out, and mv refuses: with several sources, the synopsis only accepts a directory as the last operand. The loop is the dangerous shape, and it is the shape an agent produces when it issues one tool call per file.
1. Stop on the first failed command. In bash, set -eu at the top of anything an agent runs, or mkdir../out || exit 1 on the line that matters. In cmd, check the exit code before moving on:
if not exist "..\out\" ( mkdir "..\out" if errorlevel 1 exit /b 1 ) Enter fullscreen mode Exit fullscreen mode 2. Move into a directory, never "to" a name. GNU mv -t DIR means "move all SOURCE arguments into DIRECTORY" ( man page ), so a missing folder is an error rather than a rename. A trailing slash (../out/ ) says the same thing to a human, but don't lean on it alone. GNU coreutils has changed how it treats a slashed destination before, and platforms disagree.
3. Refuse to overwrite, loudly. mv -n looks like the answer, but the GNU manual says it will "silently skip" and marks the option deprecated. An agent reading exit codes learns nothing from a silent skip. GNU coreutils 9.5 and later ( release notes ) has --update=none-fail, where "any skipped files are diagnosed and induce a failure." Both -t and --update are GNU options: macOS's BSD mv rejects them, so on a Mac install coreutils with Homebrew and call gmv. On Windows, Move-Item without -Force already errors on an existing file.
set -eu mkdir -p../out mv --update = none-fail -t../out -- a.txt b.txt c.txt test " $( ls -A../out | wc -l ) " -eq 3 Enter fullscreen mode Exit fullscreen mode 4. Read after write. That last line is the check the Gemini session never ran: count what arrived. It is cheap, and it is the step the user's own diagnosis named. If you write agent instructions, ask for it directly: after moving or deleting files, list the destination and compare counts.
All four defenses reduce the odds. A snapshot taken before every agent turn means the damage can be reversed even when they all fail. The cheapest version is git, inside a repo, on a task branch:
git add -A git commit -qm "checkpoint before agent turn" #...agent turn goes wrong... git restore --source = HEAD --staged --worktree. Enter fullscreen mode Exit fullscreen mode Restore works even when the files were moved out of the repo, because the contents are already stored in.git. Two limits: ignored files such as.env aren't in the commit, and anything the agent wrote outside the repo stays where it landed.
Claude Code and Gemini CLI both ship an undo, with different gaps. Claude Code's checkpointing captures your code before every prompt, and /rewind (or Esc twice) restores code, conversation or both. Its docs spell out the hole: "A file that only Bash commands changed isn't tracked," and for those they point you to Git. Gemini CLI's checkpointing, as its docs describe it, keeps a shadow git repo under ~/.gemini/history/ and restores with /restore. It is disabled by default, and the docs name file-writing tools such as write_file and replace as the trigger. A move run through the shell isn't one of those tools, so assume it isn't covered until you've tested it.
This is the part I built Hermes IDE for, so weigh this section with that in mind. In Hermes, ⌘N (Ctrl+Shift+N on Windows and Linux) starts each task on its own hermes/<name> branch in its own worktree, so an agent's mess lands on a branch nobody else is using. According to the README, "every agent turn is saved, so you can diff or restore any single turn, with undo." The Review Desk (⌘G, or Ctrl+Shift+G) shows changes turn by turn.
The Review Desk (⌘G) in By turn view: two turns of a Claude Code task in orders-api, turn 1's diff of src/db.ts, and a commit message drafted from the turns.
The "Agent replies" strip under each session lists the agent's replies, per the 2.3.1 release notes. Click one to see its changes or restore your files to that point. Before you count on that for a shell-run move, make one in a test repo and check that it shows up in the turn's diff. Hermes runs Claude Code, Codex, Antigravity CLI, Gemini CLI and other command-line agents, each in its own terminal.
Hermes needs a git repository. A worktree is a folder, not a sandbox, so a shell command can still write to.., and a restore brings back the project's files while a stray file written one level up stays where it landed. In a task with extra folders, the strip tracks changes in the main project only. The docs don't say ignored files are covered, so keep your own backup of.env -style files. Turn snapshots for worktree tasks are removed 14 days after the session closes.
The Review Desk's Worktrees tab: one Hermes worktree each for orders-api, web-app and orders-types under ~/code, with branch, task, age, disk size and free space.
Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Google, Anthropic, OpenAI or Nous Research. Download it or read the code on GitHub.
The Gemini session went wrong because nothing recorded what each tool call actually returned, and the agent filled the gap with a story. Tracing tool calls and their results is what my LLM Observability Pocket Guide is about, along with how to pick the tracing and eval tools that fit your team.