powershell-landmines · git:20260924.9b2bbe7 · 2026-09-24 · sha256 be0db7d8eefce238

powershell-landmines git:20260924.9b2bbe7A

Immutable. This exact content is served forever at /api/v1/blob/be0db7d8eefce238.

---
name: powershell-landmines
description: "Query-first Windows failure intelligence: preflight the fuck-powershell landmine corpus before patches, and avoid PowerShell landmines when writing or running Windows shell commands: alias traps (curl/wget), POSIX redirects (/dev/null), native stderr ErrorRecord wrapping, exit-code blindness, encoding/BOM corruption, 5.1-vs-7 divergence, quoting loss, env-var identity traps. Use BEFORE generating any powershell/pwsh command, .ps1 script, or Windows CI step. Triggers: PowerShell, pwsh, powershell.exe, .ps1, Windows shell, Windows CI, icacls, Invoke-WebRequest, 파워쉘, 윈도우 스크립트."
metadata:
  short-description: "PowerShell landmine avoidance rules + case references"
---

# powershell-landmines

Windows shell & process interoperability hazards for coding agents — backed by the
fuck-powershell failure corpus (https://github.com/lidge-jun/fuck-powershell,
live: https://lidge-jun.github.io/fuck-powershell/). PowerShell is the brand;
the corpus covers cmd.exe, Node/Bun spawn, PATH/PATHEXT, encodings, Win32 paths,
and CI runner behavior.

## MCP tools (preferred when registered)

If your host exposes `fp_preflight`, `fp_search`, `fp_errors` and `fp_case` (the
fuck-powershell MCP server, `scripts/mcp.mjs`), use them instead of the CLI below: same
operations, same risk rules, a few hundred bytes per answer, and the server reads the
corpus fresh on every call. Call `fp_preflight` before the patch, `fp_case` on the top
result, and `fp_search` / `fp_errors` over the diff and any error you hit. Registration
is in the repository README ("Use it as an MCP server"). Use the CLI only when those
tools are absent.

In Codex Code Mode these tools may not be in your own tool list at all: they sit inside
`exec` as `mcp__fuck_powershell__fp_preflight`, `..._fp_search`, `..._fp_errors` and
`..._fp_case` (look them up in `ALL_TOOLS`). Call them there before assuming they are
absent.

## CLI lookup

Pull the corpus once, then QUERY BEFORE PATCHING. Resolve the checkout in this
order and use the first that exists — do not hardcode one path, the corpus is a
working repo and it moves:

```
$FP_HOME  ->  ~/Developers/fuck-powershell  ->  ~/.fuck-powershell
```

If none exist, clone it and keep it somewhere you will actually update:

```
git clone https://github.com/lidge-jun/fuck-powershell ~/Developers/fuck-powershell
```

Before modifying code that touches Windows process execution, PowerShell/cmd
scripts, PATH/env, encodings, exit codes, or Windows CI steps, run a preflight:

```
bun $FP_HOME/scripts/fp.mjs preflight --runtime node --operation spawn --target npm --json
bun $FP_HOME/scripts/fp.mjs preflight --runtime powershell --operation encoding
```

operations: spawn | env-path | encoding | redirect | exit-code | quoting | install | ci.
Read the top cases it returns (`fp case <id>`) and apply their constraints.
After generating a diff, postflight risky tokens: `fp search "<tokens from diff>"`
and `fp errors <enoent|einval|eperm|...>` when an error signature appears.
risk: high means read the top case BEFORE writing code; medium means scan titles.
The graph is rebuilt automatically on first query; `git -C $FP_HOME pull` to update.

## When the corpus is wrong or silent

A miss is signal. If you hit a Windows failure that no case predicted, or a case
told you something this machine contradicts, **file it** — that is how the corpus
stays worth querying:

```
gh issue create --repo lidge-jun/fuck-powershell --template landmine.yml
```

Search first (`fp search`) and name the near-miss case ids in the issue, so the
distinction is explicit rather than a duplicate. Evidence bar: exact commands,
exact output, and a control run that works. Then update the corpus and reinstall
this skill — it is a **copy**, not a symlink, so it does not update itself:

```
bun $FP_HOME/scripts/install-skill.mjs --check   # has my copy drifted?
bun $FP_HOME/scripts/install-skill.mjs           # refresh it
```

## Core rules (fallback when the corpus is not installed)

1. Never use bare `curl` or `wget` — on Windows PowerShell 5.1 they are aliases of
   `Invoke-WebRequest`. Use `curl.exe` or `Invoke-RestMethod`. Never probe with
   `command -v` (silent no-op) — use `Get-Command`. Never resolve npm tools to
   their `.ps1` shim — prefer `.cmd`/`.exe`.
2. Never redirect to `/dev/null` — use `$null` (`2>$null`, `*> $null`) or `Out-Null`.
3. Check native failures with `$LASTEXITCODE`, not `$?` or try/catch. On 7.4+ you
   may set `$PSNativeCommandUseErrorActionPreference = $true`. In `shell: pwsh` CI
   steps, end expected-failure branches with explicit `exit 0` (the last native
   exit code leaks into the step result). In `irm | iex` scripts, fail with
   `throw`, never `exit` — exit kills the user's terminal.
4. Do not combine `2>&1` with `$ErrorActionPreference='Stop'` around native
   commands on 5.1 — stderr lines become fatal ErrorRecords.
5. Always pass an explicit `-Encoding` when writing files. 5.1 `Out-File`/`>`
   default to UTF-16LE+BOM; 7 defaults to BOM-less UTF-8.
6. Any .ps1 you generate that contains non-ASCII MUST be saved UTF-8 **with BOM**,
   or 5.1 parses it as the ANSI code page (CP949/CP1252) before execution.
7. Know which runtime runs your script: `powershell.exe` is 5.1, `pwsh` is 7.
   In GitHub Actions, `shell: powershell` != `shell: pwsh`. Pin deliberately.
   5.1 has NO `&&`/`||` — gate with `if ($?)`; the word `and` is never a
   separator; a `;` between fragments of ONE call splits it into broken statements.
8. Do not pass inline JSON or empty-string args to native commands on 5.1/7.0;
   quoting is rebuilt heuristically. Use files/stdin, or pin
   `$PSNativeCommandArgumentPassing = 'Standard'` on 7.2+.
9. Never build a security principal from `$env:USERDOMAIN`/`$env:USERNAME` — use
   the token SID: `[Security.Principal.WindowsIdentity]::GetCurrent().User.Value`.
   Never write `$env:Path` (merged view) into User PATH — read the 'User' scope and
   append only the missing entry. After installers, re-merge `$env:Path` from the
   Machine+User registry scopes (the session snapshot is stale).
10. Probe commands with `Get-Command`, not `command -v`; prefer a script file over
    a deep quoted one-liner; verify encodings by bytes, never by console rendering.

## References (full cases with repro + citations)

| file | covers |
|---|---|
| references/aliases.md | command-v-noop, curl-alias, get-command-where-disagree, npm-ps1-not-comspec, spawn-npm-enoent-einval |
| references/streams.md | dev-null-redirect, native-stderr-errorrecord, out-string-multiplies-stderr, write-host-not-success-stream |
| references/encoding.md | bom-less-ps1-cp949, oss-outfile-bom, tee-object-utf16, utf8-bom-still-breaks-grep |
| references/exit-codes.md | exit-code-vs-dollar-q, explorer-exits-one, if-nativecmd-truthiness, irm-iex-kills-host, pwsh-leaks-lastexitcode, start-process-no-lastexitcode |
| references/args-quoting.md | backslash-quote-ends-span, bun-ps-windowstyle-argv, cmd-shim-reparses-argv, cmd-start-ampersand-splits, dollar-backslash-vars, dq-regex-interpolates, english-and-not-separator, join-semicolon-splits-startprocess, oss-native-arg-quoting, piped-iex-drops-params, prose-as-unknown-flags, ps-file-extension-dispatch, windowstyle-hidden-vs-windowshide |
| references/versions.md | ps51-no-and-and, ps51-vs-7-split, strictmode-missing-property |
| references/env-paths.md | env-domain-principal, env-path-vs-PATH-casing, envpath-pollutes-user, node-path-host-delimiter, path-colon-not-delimiter, path-dot-hijacks-bare-npm, pathext-bare-name-enoent, pathext-exe-beats-cmd, session-path-stale, test-path-trailing-whitespace, windowsapps-alias-eperm |
| references/ci-agents.md | actions-default-shell, cmd-posix-env-prefix, execution-policy-file-block |
| references/collections.md | get-content-scalar-collapse, ne-filters-instead-of-compares, return-does-not-mean-return |
| references/parsing.md | convertto-json-depth-two, culture-comma-decimal-cast |

Read the matching reference before writing code in that risk area. Each case is
Symptom / Repro / Cause / Workaround with public commit/PR citations.