Agent Development · Automation / AGENT SKILL
agent-powershell-guidelines
0
Guide AI agents in reliable PowerShell execution, quoting, file I/O, encoding, serialization, and Windows compatibility.
The full skill.
Original instructions from the publisher’s SKILL.md
# PowerShell & Windows Shell Guidelines for AI Agents
## Overview
Development and tool execution in Windows repositories take place on **Windows**. Terminal tool commands execute directly inside **PowerShell**. These guidelines prevent syntax errors, escaping failures, execution policy blocks, encoding/line-ending corruption, process hangs, and state pollution.
> [!NOTE]
> Every behaviour asserted in this document was measured on **Windows PowerShell 5.1.26100.9168**
> (Desktop edition, Windows 11 Pro 26200) on 2026-08-25 — not recalled from documentation. Where
> a rule depends on the host, the document says how to check rather than what to assume.
> Re-verify after a major Windows or PowerShell upgrade.
---
## 1. Direct Execution — Do NOT Wrap in PowerShell
- **DO NOT** wrap commands in nested `powershell` or `powershell.exe -c` wrappers. The tool environment already runs directly in a PowerShell session.
- Running nested PowerShell processes causes inner variables (such as `$_`, `$f`, `$var`), quotes, and newlines to get prematurely expanded, stripped, or corrupted by the outer shell parser. A child process also **loses the caller's session state** — see §3 and §6.
- **DO** write PowerShell commands and CLI invocations directly into the terminal tool's command input (e.g., `Get-ChildItem -Path .`, `dotnet build`, `git status`).
---
## 2. PowerShell 5.1 Language Limits (Read First)
Windows PowerShell 5.1 is **not** PowerShell 7. Several constructs that agents write by reflex are **parse errors** — the command fails before a single line executes, so no partial work happens and the error message often does not name the real cause.
**Confirm which shell you are in before trusting anything below:**
```powershell
"$($PSVersionTable.PSVersion) $($PSVersionTable.PSEdition)" # expect e.g. 5.1.26100.9168 Desktop
```
Under PowerShell 7 (`Core`) every restriction in this section is lifted, and several encoding rules in §6 invert.
| You want to write | PS 5.1 result | Write instead |
|---|---|---|
| `A && B` | **Parse error**: `The token '&&' is not a valid statement separator in this version.` | `A; if ($?) { B }` |
| `A \|\| B` | **Parse error**, same cause | `A; if (-not $?) { B }` |
| `A; B` (unconditional) | Fine | — |
| `$c ? $x : $y` | **Parse error**: `Unexpected token '?' in expression or statement.` | `if ($c) { $x } else { $y }` |
| `$a ?? $b` | **Parse error**: `Unexpected token '??' in expression or statement.` | `if ($null -eq $a) { $b } else { $a }` |
| `$obj?.Prop` | **Parses without error — and does the wrong thing** | `if ($null -ne $obj) { $obj.Prop }` |
> [!CAUTION]
> **`?.` is the dangerous one.** `?` is a legal character in a PowerShell variable name, so
> `$null?.Length` parses cleanly as a variable named `null?` followed by `.Length`. There is no
> error to notice — the null-conditional silently means something else. The others at least fail
> loudly.
- **`ConvertFrom-Json` returns a `PSCustomObject`, not a hashtable**, and PS 5.1 has **no `-AsHashtable`** parameter. Access members with dot notation, or convert explicitly. (For the serialization direction, see the depth trap in §7.)
---
## 3. Multi-Line & Complex Logic — Use Scratch Scripts
- **DO NOT** write complex multi-line loops, pipeline blocks, or regex replacements inline in a single command string. Escaping quotes and variables across tool JSON boundaries is error-prone.
- **DO write a `.ps1` script** to the agent's dedicated scratch directory using the file-writing tool.
- **Script Execution — run it in the current session:**
```powershell
& 'C:\full\path\to\scratch\script.ps1'
```
This is the primary form. It keeps the session's `$PSDefaultParameterValues`, preference
variables, and working directory, and it does not violate §1.
- **Fallback only if a policy blocks the script** (`PSSecurityException`):
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File 'C:\full\path\to\scratch\script.ps1'
```
> [!WARNING]
> A child process **does not inherit the caller's `$PSDefaultParameterValues`**. Measured: the
> same `'x' > file` statement wrote UTF-8-with-BOM in a harness session that had pre-set an
> `Out-File:Encoding` default, but genuine **UTF-16 LE** (`FF FE`) inside
> `powershell.exe -File`. Check `Get-ExecutionPolicy -Scope Process` before reaching for this —
> many agent harnesses already run as `Bypass`, making the fallback unnecessary.
- **ASCII in Scratch Scripts:** Windows PowerShell 5.1 parses BOM-less `.ps1` files using the system ANSI code page (Windows-1252). Keep script source code ASCII-only (use Unicode escape codes like `[char]0x2014` or read UTF-8 data via .NET) to avoid script parsing syntax errors.
---
## 4. Error Handling & Native Command Quirks (Crucial)
- **Cmdlet Errors:** Place `$ErrorActionPreference = 'Stop'` at the top of your scripts to halt on PowerShell cmdlet errors.
- **Native Executables (git, dotnet, msbuild):** The Stop preference does NOT halt on native executable failures. You MUST explicitly check the process exit code:
```powershell
dotnet build
if ($LASTEXITCODE -ne 0) { throw "Build failed with exit code $LASTEXITCODE" }
```
Measured: `cmd.exe /c "exit 3"` leaves `$LASTEXITCODE = 3` and `$? = $false`, and throws nothing.
- **The Stderr Redirect Crash:** If the Stop preference is active, redirecting stderr from a native command (e.g., appending `2>&1`) will instantly throw a fatal exception if the tool writes to stderr, even if it succeeds! Measured: a command that wrote one stderr line and **exited 0** threw `NativeCommandError`. Temporarily set the preference to `'Continue'` around noisy native commands if you must capture stderr logs.
- **`-ErrorAction SilentlyContinue` does not make a failure harmless.** It suppresses the error *output*, but the cmdlet still failed and the call still reports failure. To make a cmdlet failure genuinely non-fatal, promote it to terminating and swallow it:
```powershell
try { Get-Item 'maybe-missing' -ErrorAction Stop } catch { }
```
Without `-ErrorAction Stop` a *non-terminating* error bypasses `catch` entirely — the `try` block is not a safety net on its own.
---
## 5. Quoting, Strings, and Calling Executables
- **Single vs. Double Quotes:**
- Use **single quotes** (`'...'`) by default for string literals, file paths, and regex patterns to prevent accidental variable expansion (`$`).
- Use **double quotes** (`"..."`) only when variable interpolation is explicitly needed.
- Subexpressions in double quotes: `$obj.Prop` only interpolates `$obj`. Use `$($obj.Prop)` to access properties inside double quotes.
- **Backtick Escaping:** PowerShell's escape character is the backtick (`` ` ``), NOT the backslash (`\`):
- Newline: `` `n `` | Carriage Return: `` `r `` | Tab: `` `t `` | Literal double-quote: `` `" `` | Literal dollar sign: `` `$ `` | Literal backtick: ```` `` ````
- **Executables with Spaces in Path:**
- Prefix with the call operator `&` when the executable path is quoted or stored in a variable:
```powershell
& "C:\Program Files\dotnet\dotnet.exe" build
& $msbuildPath build\tools\Generator.vcxproj
```
- Without `&`, PowerShell treats the quoted string as a literal and outputs the text instead of running the program.
- **Multi-line arguments to native executables — use a here-string.** Commit messages, PR bodies, and file content passed on a command line are the usual cases:
```powershell
git commit -m @'
Fix the thing.
Second line with $literal dollar signs and `backticks` left alone.
'@
```
- Use `@'...'@` (**single**-quoted, literal) so `$` and backticks are not expanded. `@"..."@` interpolates — only use it when you want that.
- The closing `'@` **must be at column 0**, on its own line. Indenting it is a parse error.
- **Arguments PowerShell wants to parse as operators:** use the stop-parsing token `--%` to pass the rest of the line through verbatim:
```powershell
git log --% --format=%H
```
---
## 6. File Read/Write: Encoding, Redirection, and Line Endings
This is the single most common area where AI agents corrupt files on Windows.
- **NO REDIRECTION OPERATORS (`>` / `>>`):**
**DO NOT** use `>` or `>>` to write or append to text files. What you get depends on the session, and **every** variant is wrong for a tree that requires BOM-less UTF-8:
| Context | Bytes from `'hello' > file` |
|---|---|
| Stock PS 5.1 (including any child `powershell.exe`) | `FF FE …` — **UTF-16 LE** |
| A session where the harness pre-set `$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'` | `EF BB BF …` — **UTF-8 with BOM** |
Both break git diffs and compilers. Because the outcome is host-dependent, do not reason about
which one you will get — use the .NET methods below instead. (`>` and `>>` are aliases for
`Out-File`, so the same applies to it.)
- **`Set-Content` / `Add-Content` write ANSI**, not UTF-8. Measured: an em-dash became the single byte `97` (Windows-1252), which does not round-trip. Pass `-Encoding utf8` if you must use them — but note that PS 5.1's `utf8` **adds a BOM**, so prefer .NET.
- **Writing/Appending Files (BOM Prevention):**
Use .NET methods to write clean BOM-less UTF-8:
```powershell
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($absPath, $text, $utf8NoBom)
[System.IO.File]::AppendAllText($absPath, "appended text`r`n", $utf8NoBom)
```
- **Absolute Paths with .NET Methods (Beware Resolve-Path):**
`[System.IO.File]` methods use the underlying .NET working directory, which does **not** track PowerShell's `$PWD` variable. Measured: after a `Push-Location`, `$PWD` pointed at the new directory while `[Environment]::CurrentDirectory` still pointed at the original.
- Always pass absolute paths to .NET methods.
- **DO NOT use the `Resolve-Path` cmdlet for creating new files** — it throws `ObjectNotFound` if the file does not exist yet.
- **DO use `$PWD`** to construct absolute paths safely: `Join-Path $PWD.Path 'newfile.txt'`.
> `$PWD` is a `PathInfo` object, not a string. `"$PWD\newfile.txt"` happens to work on a
> FileSystem drive, but on any other PSDrive the interpolation yields a provider-qualified
> path that .NET cannot open. `$PWD.Path` is the safe form.
- **Reading Files (Preserving Line Endings & UTF-8):**
- `Get-Content` **strips** `\r\n` line endings and returns a string array unless you pass `-Raw`.
- `Get-Content` honours a **BOM**, and falls back to ANSI (Windows-1252) only when there is none. Measured on the same em-dash content, with no `-Encoding`:
| Input file | Result |
|---|---|
| UTF-8 **without** BOM | `em-dash: —` — mojibake |
| UTF-8 **with** BOM | `em-dash: —` — correct |
Because a BOM-less tree is the convention here, **`-Encoding UTF8` is mandatory on every read** — otherwise non-ASCII silently corrupts, and only on the files that have any.
- To read full raw text cleanly while preserving UTF-8 and exact line endings:
```powershell
# Recommended (.NET UTF-8):
$text = [System.IO.File]::ReadAllText($absPath, [System.Text.Encoding]::UTF8)
# Or native PowerShell with explicit UTF-8 and -Raw:
$text = Get-Content -Path $absPath -Encoding UTF8 -Raw
```
- **In-Place File Text Replacement (sed equivalent):**
- **Literal replacement (recommended for exact code snippets):**
```powershell
$f = Join-Path $PWD.Path 'path\to\file.txt'
$t = [System.IO.File]::ReadAllText($f, [System.Text.Encoding]::UTF8).Replace('exact_old', 'exact_new')
[System.IO.File]::WriteAllText($f, $t, (New-Object System.Text.UTF8Encoding($false)))
```
- **Regex replacement:**
```powershell
$f = Join-Path $PWD.Path 'path\to\file.txt'
$t = [System.IO.File]::ReadAllText($f, [System.Text.Encoding]::UTF8) -replace 'pattern', 'replacement'
[System.IO.File]::WriteAllText($f, $t, (New-Object System.Text.UTF8Encoding($false)))
```
- **Detecting Line Endings (CRLF vs LF):**
Do NOT use Unix utilities like `grep` or `file` — MSYS and WSL shells open files in text mode and strip CR in memory, so they report LF for a CRLF file **with no error**. Count the bytes via .NET instead; in a clean CRLF file the two counts are equal:
```powershell
$b = [System.IO.File]::ReadAllBytes($absPath)
"CR=$(@($b | Where-Object { $_ -eq 0x0D }).Count) LF=$(@($b | Where-Object { $_ -eq 0x0A }).Count)"
```
Inside a git working tree, `git ls-files --eol <path>` reports both sides at
once (`i/` = index, `w/` = working tree). When modifying an **existing** file,
match whatever `w/` reports, and never mix conventions within one file.
- **Which convention to use: CRLF, with two kinds of exception.**
| File | Convention | Why |
|------|-----------|-----|
| Everything, by default | **CRLF** | Every repository here declares `* text=auto eol=crlf`. Git stores LF-normalized content either way; only the checkout is CRLF |
| `*.yml`, `*.yaml`, `*.py` consumed by CI | **LF** | Checked out on a Linux runner. `eol` in `.gitattributes` applies on **every** platform, so a CRLF workflow puts a trailing `\r` on each line of every `run:` block and bash dies with `` $'\r': command not found `` -- an error naming a command that does not exist, not a line ending |
| A file that disagrees with the above | **Match the file** | The repository you are in may not have been converted. `git ls-files --eol` is the authority, not this table |
> [!CAUTION]
> **Do not "fix" an LF file sitting in a CRLF repository until you know why it is
> LF.** In `SharedAgentSkills`, `.github/workflows/*.yml` and `tools/*.py` are LF
> *deliberately*, pinned in both `.gitattributes` and `.editorconfig` with the
> reason written beside them. Normalizing them breaks CI, and the failure points
> somewhere else entirely.
**Practical consequences.**
- `core.autocrlf` is `false` on these machines, overriding the system-level
`true`. Git therefore converts **nothing** on checkout beyond what
`.gitattributes` `eol` dictates, and it corrects nothing on the way in: the
bytes a tool writes are the bytes that get committed.
- Harness file-writing tools generally emit **LF**. A file you create or fully
rewrite needs converting before you hand the work back:
```powershell
$enc = New-Object System.Text.UTF8Encoding($false)
$t = [System.IO.File]::ReadAllText($abs)
[System.IO.File]::WriteAllText($abs, (($t -replace "`r`n","`n") -replace "`n","`r`n"), $enc)
```
- Converting line endings produces **no Git diff**: the stored blob is already
LF-normalized, so `git add` on a converted file stages nothing. `git status`
may still list it as modified -- that is a stale index `mtime`, not a change.
Clear it with `git add --renormalize . ; git reset -q`, and do that **before**
reviewing a diff, or real changes are buried among phantom ones.
- Because the conversion is invisible to Git, another developer's working tree
does **not** change when they pull. `eol` is applied at checkout, not at pull.
---
## 7. Dangerous PowerShell 5.1 Quirks
- **The JSON Truncation & BOM Bug:**
`ConvertTo-Json` in PS5.1 defaults to a serialization depth of **2**. If you convert deeply nested objects (like `package.json` or `appsettings.json`), it silently truncates objects into the literal string `"System.Collections.Hashtable"`, destroying the configuration. Measured: `@{a=@{b=@{c=@{d='leaf'}}}}` serialized to `{"a":{"b":{"c":"System.Collections.Hashtable"}}}`. Furthermore, piping to `Set-Content` or `Out-File` inserts a UTF-8 BOM or writes ANSI.
- **Correct BOM-less JSON serialization:**
```powershell
$json = $obj | ConvertTo-Json -Depth 100
[System.IO.File]::WriteAllText($absPath, $json, (New-Object System.Text.UTF8Encoding($false)))
```
- **Never assume shell state survives a tool call.** Many agent harnesses run each command in a fresh PowerShell process, keeping only the working directory. Variables, functions, imported modules, preference variables, and `$env:` values are then discarded between calls.
- **Default: assume nothing persists.** Set everything a command needs *within that same command*, or write it to a scratch file. Never split a `$var = …` from its use across two tool calls.
- **Where state *does* persist, clean up after yourself.** `$env:VAR = 'val'` then leaks into every later call; unset with `$env:VAR = $null` when finished.
- **Probe it if it matters** — set a marker in one call, read it in the next:
```powershell
$env:PROBE = 'x' # call 1
if ($env:PROBE) { 'persists' } else { 'discarded' } # call 2
```
> [!NOTE]
> The default above is deliberately the pessimistic one, and should stay that way. Assuming
> persistence that is not there makes an agent silently read an empty value and act on wrong
> data; assuming the reverse costs one redundant assignment. The failure modes are not
> symmetric.
- **Command Prompt Path Separator Rule:** Paths passed to `cmd.exe` utilities (like `rmdir`) **MUST use Windows backslashes (`\`)**. Measured: `cmd /c "rmdir /s /q C:/path/fs"` printed `Invalid switch - "fs".`, exited 1, and left the directory in place; the backslash form exited 0 and removed it. Note the failure is **silent in effect** unless you check the exit code.
- **Web Requests (curl / wget equivalents):** In PS 5.1 both names are **aliases** for `Invoke-WebRequest`.
- You **MUST** append `-UseBasicParsing` to avoid the legacy Internet Explorer DOM parser, which is absent from Windows 11 and fails outright in headless sessions.
- **Do not blindly force TLS.** Measured `[System.Net.ServicePointManager]::SecurityProtocol` = `SystemDefault`, the .NET Framework 4.7+ default, which delegates to the OS and already negotiates TLS 1.2/1.3. Hard-assigning `Tls12` **pins** the process to TLS 1.2 and *disables* TLS 1.3 — a downgrade, not a fix. Check the value first, and only set it if a request actually fails a handshake and the reading is not `SystemDefault`.
- To get the **real curl** rather than the alias, invoke `curl.exe` explicitly (present at `C:\Windows\system32\curl.exe` on Windows 10+, but shadowed by the alias). Otherwise Unix-style flags produce `Invoke-WebRequest` parameter-binding errors that look nothing like a curl diagnostic.
---
## 8. Non-Interactive Execution — Avoid Hangs
An agent's shell has no usable stdin. A command that waits for input does not error — it **blocks until the tool times out**, which is strictly worse than failing, because it burns the whole call and produces no diagnostic.
- **Never call** `Read-Host`, `Get-Credential`, `Out-GridView`, `$Host.UI.PromptForChoice`, or `pause`.
- **Destructive cmdlets may prompt for confirmation.** Pass `-Confirm:$false` when you intend the action to proceed (`Remove-Item`, `Stop-Process`, `Clear-Content`, …), and `-Force` for read-only or hidden items.
- **Never run interactive git**: `git rebase -i`, `git add -i`, `git commit` with no `-m`, or anything else that opens an editor. Add `--no-pager` to reading commands (`git --no-pager diff`, `git --no-pager log`) so the pager cannot block.
- **Prefer explicit non-interactive flags** on other CLIs too — e.g. `npm ci` over a prompt-capable install, `dotnet` commands with all arguments supplied.
---
## 9. Common Command Substitutions (No Unix Coreutils)
Use native PowerShell cmdlets or robust Windows alternatives:
| Unix / Bash Command | PowerShell / Windows Equivalent | Notes |
|---|---|---|
| `head -n 20 file.txt` | `Get-Content file.txt -Encoding UTF8 -TotalCount 20` | Output line by line |
| `tail -n 20 file.txt` | `Get-Content file.txt -Encoding UTF8 -Tail 20` | Output line by line |
| `cat file.txt` | `Get-Content file.txt -Encoding UTF8 -Raw` | Read entire file as single string |
| `grep "pattern" file.txt` | `Select-String -Pattern 'pattern' -Path file.txt -Encoding UTF8` | Match lines in file |
| `grep -r "pattern" dir/` | `Get-ChildItem -Recurse -File dir \| Select-String -Pattern 'pattern' -Encoding UTF8` | Recursive file search |
| `find . -name "*.cs"` | `Get-ChildItem -Recurse -Filter '*.cs'` | File search |
| `rm -rf dir/` | `Remove-Item -Path 'dir' -Recurse -Force` | For deep/locked trees run `rmdir /s /q` via `cmd.exe` — **backslashes only**, see §7 |
| `mkdir -p dir/subdir` | `New-Item -ItemType Directory -Force -Path 'dir/subdir' \| Out-Null` | `-Force` is safe here — it is a no-op on an existing directory |
| `touch file.txt` | `if (-not (Test-Path 'file.txt')) { New-Item -ItemType File 'file.txt' }` | **Never `New-Item -ItemType File -Force`** — see warning below |
| `which tool` | `(Get-Command tool -ErrorAction SilentlyContinue).Source` | Without `-ErrorAction` a missing tool emits a **non-terminating** error: `.Source` is `$null`, `catch` does **not** fire (unless `$ErrorActionPreference = 'Stop'`), and the console fills with noise. Always test for `$null` |
| `wc -l file.txt` | `(Get-Content file.txt -Encoding UTF8 \| Measure-Object -Line).Lines` | Count lines |
| `curl -s URL` | `Invoke-WebRequest URL -UseBasicParsing` | `curl`/`wget` are aliases for this; use `curl.exe` for real curl (§7) |
| `export VAR=val` | `$env:VAR = 'val'` | Scope is one command unless the harness reuses its shell — see §7 |
| `unset VAR` | `$env:VAR = $null` | Only needed where state persists — see §7 |
| `sed -i 's/a/b/' f` | `.NET` read-replace-write | See §6; there is no safe in-place one-liner |
| `git diff` | `git --no-pager diff` | Prevents interactive pager hang |
> [!WARNING]
> **`New-Item -ItemType File -Force` truncates an existing file to 0 bytes.** Measured: a file
> containing `important` had length 0 afterwards, with no error and no confirmation prompt.
> `-Force` is safe for *directories* (the `mkdir -p` row above) and destructive for *files* — do
> not carry the habit across. Guard a `touch` equivalent with `Test-Path`.
---
## 10. Installing Tools -- Ask, Do Not Skip
When a task needs a tool, package, or library that is not installed, **ask the user
whether to install it.** Do not silently drop the step, weaken the approach, or
substitute a worse method to avoid the install.
- **Ask; do not install unprompted.** Installing changes the user's machine, so it is
their decision -- but it is a decision they must actually be given.
- **Do not quietly work around a missing tool.** Skipping a verification step, replacing a
real parser with a regex heuristic, or downgrading a check to "probably fine" produces
weaker work while looking complete. That is worse than pausing to ask.
- **Say what is missing and what it buys.** Name the tool, the command that would install
it, and what becomes possible with it. "PyYAML is not installed -- with it I can
actually parse every frontmatter block instead of pattern-matching them; install with
`python -m pip install pyyaml`?"
- **If the user declines**, proceed with the best available approach and **state plainly
in the final report** which check was weakened or skipped, and how.
- **Prefer project-local and already-declared dependencies.** If the tool belongs in
`package.json` or a `.csproj`, adding it there is a project change and needs a plan, not
just an install.
---
## 11. Harness-Specific Integration
### Harness: Claude Code
1. **Direct Execution:**
- Write PowerShell commands directly without nested `powershell -Command` wrappers.
- Run scratch scripts in-session with `& 'C:\full\path\script.ps1'`. Claude Code's session already runs with execution policy `Bypass`, so fallback flags are unnecessary and discard session encoding defaults.
2. **`Bash` Tool vs PowerShell:**
- Claude Code provides both PowerShell and a Git Bash `Bash` tool. Default to PowerShell.
- Never use Git Bash or WSL `grep`/`head`/`file` to check line endings as they silently strip CR.
3. **Native File Tools:**
- Always use Claude Code's native file tools (`Write`, `Edit`, `Read`, `Grep`, `Glob`) rather than shell cmdlets where possible.
- `Write` emits LF line endings. Normalize newly created files to CRLF if matching a CRLF working tree.
4. **Exit Codes & State:**
- Always check `$LASTEXITCODE` after native CLI commands (`dotnet`, `git`, `msbuild`, `npm`).
- If probing an intentional failure, reset with `$global:LASTEXITCODE = 0` so the call is not marked as failed.
- Only the working directory persists between calls; assume variables and `$env:` are discarded.
5. **Encoding Defaults:**
- Claude Code pre-sets `$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'`.
### Harness: Antigravity
1. **Scratch Scripts:**
- Save temporary files and scratch scripts to `<appDataDir>\brain\<conversation-id>\scratch\`.
2. **Execution Policy Checks:**
- If executing a scratch `.ps1` script fails due to `PSSecurityException`, check `Get-ExecutionPolicy -Scope Process`. If not `Bypass`, use `powershell.exe -NoProfile -ExecutionPolicy Bypass -File <path>`.
3. **Native File Tools:**
- Use `write_to_file`, `replace_file_content`, and `view_file` for file operations.
### Harness: Codex
- `%TEMP%` is writable; keep scratch files there rather than in a repository.
- `apply_patch` can leave mixed line endings in a CRLF file; normalize edited Markdown and PowerShell files to CRLF afterward.
- `workspace-write` permits the current workspace and configured `sandbox_workspace_write.writable_roots`; a listed root may still prompt for approval on Windows.
- `.git`, `.agents`, and `.codex` remain protected inside writable roots; request an approved escalation before writing them.
- Require exit code 0 from tool-run native commands; inside PowerShell scripts, check `$LASTEXITCODE` explicitly.