You paste the command an agent just handed you into a Windows terminal, and it dies on the first &&. If you work across platforms as an indie developer, that one line has probably cost you an afternoon at some point.
Antigravity is not the problem here. The destination is simply different. On macOS and Linux the command lands in bash or zsh. On Windows it lands in PowerShell, and most of the one-liners a model has absorbed were written for the first group.
Hub 2.9.1 on August 20 added syntax highlighting for .ps1, .psm1, .psd1, and for pwsh / ps1 code fences. I read that as a sign that writing PowerShell is becoming a first-class expectation rather than an afterthought. So I installed PowerShell 7.6.5 locally and pushed the shapes an agent typically produces through it, one at a time.
The thing that surprised me was not &&. It was the exit codes.
Whether && works depends on which PowerShell you have
Start by finding out which one you are running. It takes one line.
$PSVersionTable.PSVersion
$PSVersionTable.PSEditionMy environment returned 7.6.5 and Core. Core means the PowerShell 7 line. Desktop means Windows PowerShell 5.1, the one that ships with Windows. If you have not installed anything, you are almost certainly on the latter.
That generation gap collides directly with what agents produce.
| Behavior | Windows PowerShell 5.1 | PowerShell 7.x |
|---|---|---|
&& / || chaining | Reserved, rejected as a syntax error | Supported |
curl / wget | Aliases for Invoke-WebRequest | No alias; the real binary runs |
Out-File default encoding | UTF-16LE with BOM | UTF-8 without BOM |
| How you get it | Bundled with Windows | Installed separately |
I ran the chaining case on 7.6.5 to be sure.
pwsh -NoProfile -c 'true && echo "chained-ok"'
# chained-okIt works. Hand the same string to 5.1 and it never executes at all, because parsing fails first. The agent then receives something that is not "the command failed" but "the command was never understood," so it rewrites and gets rejected again. That loop burns turns.
I also measured startup cost while I was there. Five runs of pwsh -NoProfile -c '1' came in at 228, 211, 217, 217, and 232 milliseconds. On the same machine, bash -c 'true' took 3 milliseconds every time. If an agent fires dozens of shell commands per task, that difference alone adds ten seconds or more. -NoProfile is still worth keeping, since it skips profile loading on top of that baseline.
When an agent writes curl, 5.1 runs something else entirely
I checked the alias table on 7.6.5.
Get-Alias curl -ErrorAction SilentlyContinue # nothing
Get-Alias wget -ErrorAction SilentlyContinue # nothingThe 7 line dropped both aliases, so curl -s https://example.com reaches the actual curl binary.
On 5.1 those aliases are still defined and point at Invoke-WebRequest. Flags like -s and -H are not parameters that cmdlet knows, so what comes back is a parameter-binding error, not a network error. Nothing in the message mentions HTTP at all, which sends you looking in entirely the wrong place.
The fix is boring and reliable: write external commands with their extension.
curl.exe -s https://example.com
node.exe --versionOne honest caveat. Aliases such as ls and cat behave differently by platform even inside the 7 line, and what I verified was the linux-x64 build of 7.6.5. The only claims I am willing to base on my own measurement are curl and wget. For everything else, run Get-Alias once on your Windows box and look at the list yourself.
Failures collapse to 1, and that breaks automated success checks
This is the part that held my attention. When you pass a command with -c (-Command), the exit code from inside does not always reach the outside intact. Here is what I measured.
| What I ran | Inner exit code | What the caller received |
|---|---|---|
pwsh -NoProfile -c 'exit 3' | 3 | 3 |
pwsh -NoProfile -c 'exit 42' | 42 | 42 |
External command inside -c exits 3 | 3 | 1 |
Same, plus ; exit $LASTEXITCODE | 3 | 3 |
ls on a missing path inside -c | 2 | 1 |
Same, plus ; exit $LASTEXITCODE | 2 | 2 |
pwsh -NoProfile -File ./t.ps1 containing exit 3 | 3 | 3 |
Failing command followed by || echo fallback | 2 | 0 |
When you write exit yourself, the value survives. When an external command fails, 3 and 2 are flattened to the same 1. You still learn that something failed; you lose how it failed. If anything downstream branches on the specific code, that distinction is gone.
The last row is the sneakiest. Once the right side of || succeeds, the whole invocation reports 0. Your log shows a failure while your caller records a success.
Reading $? has its own timing rule.
# read immediately: False
& ./somecommand ; $q = $?; "q=$q code=$LASTEXITCODE" # q=False code=3
# put any statement in between and it flips back
& ./somecommand ; "anything" | Out-Null; "q=$? code=$LASTEXITCODE" # q=True code=3$? only remembers the single statement before it. $LASTEXITCODE keeps the external command's value, so that is the one to branch on.
My own rule is short: if you use -c, end it with exit $LASTEXITCODE; when you can, pass a script with -File instead. Something similar happens on the bash side for entirely different reasons, which I wrote up in the pipe in my wrapper was swallowing Antigravity CLI's exit code. Different cause, very familiar symptom.
Logs written through redirection change bytes between generations
Having an agent write its run log to a file, then reading it yourself later, is a common enough pattern. The generation gap shows up here too.
I wrote a file on 7.6.5 and looked at it byte by byte.
"日本語ログ" | Out-File -FilePath ./enc7.txt
(Get-Item ./enc7.txt).LengthThe file came to 16 bytes for that five-character Japanese string, and the leading bytes were e6 97 a5 e6 9c ac e8 aa 9e e3 83 ad e3 82 b0 0a — UTF-8 with no BOM, plus one newline. grep reads it, Python reads it, nothing special required.
On 5.1, Out-File and > default to UTF-16LE with a BOM. The same five characters roughly double in size and gain a byte-order mark. Open the file and it looks correct to you; hand it to a parser or back to an agent and every character appears to have a null byte wedged next to it. Most of the times a log looked "empty" to me, this was why.
State it explicitly when you write.
"日本語ログ" | Out-File -FilePath ./log.txt -Encoding utf8Note that utf8NoBOM only exists from 7 onward. If you are staying on 5.1 and want to avoid the BOM, [System.IO.File]::WriteAllText() is the dependable route.
That covers files you write. If the terminal display itself is garbled, the cause sits somewhere else entirely, so start with fixing Japanese mojibake in Antigravity's integrated terminal.
Put these assumptions in a rules file, not in the conversation
You could just say all of this in chat every time. It does not hold up. Long conversations bury it, and a new session forgets it.
Antigravity lets you keep rules as files. The Customizations panel in hub 2.9.1 lists skills, rules, plugins, and custom agents as separate collapsible sections, and from CLI 1.1.15 onward a markdown-defined agent can name rule files directly in its frontmatter with rules:. That is the shape you want when you would rather apply a few specific rules than inherit an entire tree.
What I keep for Windows is about this short.
---
name: windows-shell
description: Shell assumptions for commands issued on Windows
---
- Assume commands run under PowerShell 7 (pwsh)
- Write external commands with their extension (curl.exe / node.exe)
- Pass -Encoding utf8 explicitly when writing files
- End any -c script with exit $LASTEXITCODEThe agent picks it up by name.
---
name: release-helper
rules:
- windows-shell.md
---Whether a written assumption is actually in effect is a separate question. Configuration can be wrong in a way that gets silently ignored rather than rejected. I covered how to find settings that are quietly doing nothing in how silently ignored config keys slip through CI, and where to block them. Worth reading before you add more rules rather than after.
What to do next
Open your Windows machine and run $PSVersionTable.PSEdition once. If it returns Desktop, installing PowerShell 7 is your first step, and three of the four issues in this article disappear on their own.
I still stop at the same places every time I cross platforms. If the numbers I measured save someone else a little of that time, that is a good outcome.