D
P
0
← All articles Baca dalam Bahasa Indonesia

CSS Layout, Overflow & Cascade

Commit Message Reduced to `@'`? A PowerShell Here-String Run in CMD

· · 7 min read
Commit Message Reduced to `@'`? A PowerShell Here-String Run in CMD

On the night of 1 May 2026 I pushed a series of commits to a client repo, then opened the log just to confirm the ordering before closing the laptop. No errors, no rejection from the remote, not a single command that stopped halfway. What greeted me instead was three identical subject lines that said nothing at all.

$ git log --format="%s" -3
@'
@'
@'

Three commits landed with @' as their entire message. Not a message truncated in the middle, not a message that lost its body and kept its subject. Those two characters were the whole commit message, and all three were already on the remote.

The message was drafted as a here-string

All three commits had long messages. A subject, a blank line, then a few explanatory bullets. For messages shaped like that I habitually reach for a PowerShell here-string, because the newlines survive as written and escaping stops being something I have to think about:

git commit -m @'
feat(module): summary of the change
 
- first bullet
- second bullet
'@

The @' at the start and the '@ at the end are the here-string markers. As long as PowerShell is the thing reading that command, everything between the two markers arrives at the -m argument as a single string, blank line included. The form is genuinely comfortable, and that is exactly what makes it dangerous.

My terminal was not PowerShell

The environment I was working in reported Platform: win32 and Shell: PowerShell in its header. I read that line and treated it as truth. But a header like that describes the system, not the terminal window actually sitting open in front of me, and the one open at the time was CMD.

CMD has no here-string syntax at all. There is no marker for it to recognise, so @' means nothing beyond literal text. The -m argument received those two characters as written, and the opening line of the message became the entire message. Git had no reason to object either, because from where it stood it had just been handed a valid, non-empty, single-line string.

That also explains why there were three of them. I ran the same shape three times in a row, none of them stopped with an error, so nothing held me back from continuing.

The cheapest way to tell the two shells apart is not reading a header but looking at the prompt:

C:\path>          CMD, no prefix at all
PS C:\path>       PowerShell, PS in front

Cleaning up three commits already pushed

Since the contents were correct and only the messages were broken, I rolled the three commits back without touching a single file. The three in HEAD~3 is exactly the number of mangled commits:

git reset --soft HEAD~3
# re-stage, then commit again with the right messages
git push --force

Force-pushing a branch other people have already seen always carries consequences, and that is the least pleasant part of this whole episode. The mistake itself was trivial, but the cost of repairing it dragged already-public history along with it.

The rule now: the message lives in a file, not in the command

The fix I settled on was not hunting for a CMD flavour of here-strings. It was to stop entrusting message text to any shell at all. Multi-line messages get written to a file first, and the commit points at that file:

git commit -F C:\temp\msg-1.txt
git log -1 --format="%H %s"

-F is git-native rather than a shell feature, so it behaves identically in CMD, in PowerShell, and in bash. One file per commit, named in sequence, and the message text never gets embedded in a command in the first place.

The second line matters as much as the first. I check every commit with git log -1 --format="%H %s" before moving on to the next one, and if the subject does not match what I intended, I stop right there. The same mistake three times in a row is not three mistakes, it is one mistake left to compound.

The rule does not apply to every commit. For trivial single-line commits, git commit -m "..." is still fine, because a single line does not collide with anything in the shell. What I reserve the file approach for is any message with paragraphs, bullets, blank lines, or anything at all longer than one line. On Windows, which is my day-to-day primary platform, that one-line boundary is the trigger I use. The same rule carries over to pull request bodies via gh pr create --body-file, since the problem there is identical.

The message files themselves are not worth fussing over. If one gets left behind in a temp folder, nothing breaks. Deleting them after the push is purely a tidiness matter.

What I struck off my habits

Four forms I have not used since:

If I ever genuinely need a PowerShell-only construct, whether a here-string, a ternary, or a pipeline chain, I confirm the shell before the command runs. The environment header has been wrong once already, and once is enough.

The second carriage: git add -A sweeping up scratch scripts

The same pattern, a convenient command whose reach is wider than I assumed, burned me again on 21 July 2026. I routinely create scratch-*.ts scripts in the repo root for end-to-end verification, things like creating and then deleting a test document. My deploy flow uses git add -A, and that command stages any such scratch script still lying around in the working tree. The script got committed and pushed to both remotes, one of which is publicly visible. Cleaning it out took a follow-up git rm commit plus another deploy.

The scripts themselves are harmless at runtime. They are .ts files at the root, not inside app/, and nothing imports them. But harmless is not a reason to let them land in a client repo.

rm -f scratch-*.ts
git status --short
git add src/util.ts src/page.tsx

Those three lines are three different exits, and any one of them is enough. Delete the scripts before the deploy commit, or read git status --short and confirm that only the intended files are staged, or drop -A and name the paths one at a time. The more durable fix, putting scratch-* in .gitignore, is still only a proposal. I have not acted on it yet.

What I took away