📝 Originally published (in Japanese) at forge.workstyle.tech.
TL;DR An unquoted empty command substitution can disappear through word splitting. git branch may then succeed with its starting point omitted, so && continues to git reset --hard. Quote command substitutions and verify the backup before resetting the working tree.
What I noticed using a disposable repository
Before executing the process, I ran one of the sessions using a disposable repository. There, I noticed that even if the part creating the backup destination fails, the git branch command itself can still succeed.
In short, the reproduction is as follows:
$ git branch orphan/x $(false) # exit=0 Branch is created at HEAD → passes through &&
$ git branch orphan/x "$(false)" # fatal: Not a valid object name: '' → stops (exit 128)
The result of $(false) is empty. If not quoted, that empty result disappears due to word splitting. The actual command passed to the shell is simply git branch orphan/x.
In this form, it qualifies as a valid git branch command with the starting point omitted. A branch pointing to HEAD is created, and the exit code is 0. Therefore, && proceeds, and the subsequent reset --hard is executed.
This makes it look as if the backup was successful even though the destination was never created. In the actual procedure, I was on the verge of losing the changes in the working tree without actually backing them up.
On the other hand, if you quote it as "$(false)", the empty argument remains. Git receives an empty starting point and stops with an error. Since the exit code is 128 here, it does not proceed past the &&.
Optional Arguments Can Take On a Completely Different Meaning
The real danger is that even after a variable evaluates to empty, the command still remains valid as a form with an omitted argument.
In this case, git branch succeeds even if you omit the starting commit. So while the command looks virtually identical, it behaves completely differently from the intended behavior of branching off a specific backup commit.
Other commands that require similar caution include git checkout -B, git reset, and rm -rf $DIR/. If you directly pass a value that might end up empty into a command that still works—with a different meaning—when an argument is omitted, the failure won't show up in the exit code.
The && operator is meant to prevent subsequent commands from running if the preceding one fails. However, if that failure isn't reflected in the command's exit code, there is nothing to stop the execution flow. Whether an error message is printed alone won't tell you if the process will actually halt.
Why I missed it
There were three sessions that were approved after reading these instructions. Everyone missed the flaw. One of them even agreed, noting, "Since they are linked with &&, it will stop if it fails."
I think in every session, people looked at the command structure and mentally assumed that "if the evacuation fails, the exit code will also be a failure." However, in reality, the result of the command substitution was empty, the arguments vanished, and git branch succeeded regardless. You couldn't verify that specific change just by reading the code.
I only noticed it because I ran it once in a disposable repository before execution. You shouldn't judge based solely on how the commands look; it is necessary to provide failing input and verify exactly what arguments the command receives.
Key takeaways
- Quote command substitutions so empty values remain arguments.
- Verify the backup succeeded before resetting the working tree.
- Test failure cases in a disposable repository.
- Make errors stop destructive follow-up commands and show a concrete reason.
Top comments (0)