powershell-landmines · git:20260923.b95267f · 2026-09-23 · sha256 6939dd7824f77cce

powershell-landmines git:20260923.b95267fA

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

---
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.

## 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.