Windows SOPS Write Patterns — Canonical Reference
Single source of truth for SOPS file operations on Windows.
Anything that writes or modifies asecrets/*.env.sopsfile on Windows must follow these patterns.
Referenced by:CLAUDE.md §Secrets,secret-manager.md §Windows SOPS,sops-edit-operations.md,credential-rotation.md.
Prerequisite — age key
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"Pattern 1 — In-place modify (add or update a key)
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
# 1. Decrypt to variable — NEVER print $plain
$plain = sops --decrypt --input-type dotenv --output-type dotenv secrets\<file>.env.sops
if ($LASTEXITCODE -ne 0) { throw "Decrypt failed" }
# 2a. ADD new key (value from $env:NEW_VALUE — never hardcoded, never printed)
$content = ($plain -join "`n") + "`n" + "NEW_KEY=$env:NEW_VALUE" + "`n"
# 2b. UPDATE existing key value (replace in memory — comment out 2a and use this)
# $content = ($plain -join "`n") + "`n"
# $content = $content -replace "(?m)^KEY_NAME=.*$", "KEY_NAME=$env:NEW_VALUE"
# 3. Write using WriteAllText — MANDATORY on Windows
# Never use >, Out-File, or Set-Content (all add CRLF or BOM → SOPS timestamp parse fails)
[System.IO.File]::WriteAllText("$PWD\secrets\<file>.env.sops", $content,
[System.Text.UTF8Encoding]::new($false))
# 4. Encrypt in-place (path matches .sops.yaml rule → correct recipients auto-selected)
sops --encrypt --in-place --input-type dotenv --output-type dotenv secrets\<file>.env.sops
if ($LASTEXITCODE -ne 0) { throw "Encrypt failed" }
# 5. Canary — mandatory before git add
sops --decrypt --input-type dotenv --output-type dotenv secrets\<file>.env.sops | Out-Null
if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }
# 6. Clear sensitive vars immediately
$env:NEW_VALUE = ""Pattern 2 — Temp file edit
When intermediate edits are complex (multiple replacements, editor-style workflow):
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$SOPS_FILE = "$PWD\secrets\<file>.env.sops"
$TEMP_FILE = "$PWD\secrets\<file>-edit.env.sops" # ← MUST end in .env.sops (path_regex in .sops.yaml)
# 1. Decrypt to temp file using WriteAllText
$plain = sops --decrypt --input-type dotenv --output-type dotenv $SOPS_FILE
[System.IO.File]::WriteAllText($TEMP_FILE, ($plain -join "`n") + "`n",
[System.Text.UTF8Encoding]::new($false))
# 2. Modify in-memory or via Get-Content pipeline (keep value in $env: vars, never print)
# 3. Re-encrypt to original file
sops --encrypt --input-type dotenv --output-type dotenv --output $SOPS_FILE $TEMP_FILE
$exitCode = $LASTEXITCODE
# 4. ALWAYS clean up plaintext — even on failure
Remove-Item $TEMP_FILE -Force -ErrorAction SilentlyContinue
if ($exitCode -ne 0) { throw "SOPS encrypt failed — original file unchanged" }
# 5. Canary
sops --decrypt --input-type dotenv --output-type dotenv $SOPS_FILE | Out-Null
if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }Temp file naming rule: must match path_regex: (^|[/\\])secrets[/\\].*\.env\.sops$
A name like monitoring-edit.env fails with no matching creation rules found.
Canary — standalone (run before every git add on a SOPS file)
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
sops --decrypt --input-type dotenv --output-type dotenv secrets\<file>.env.sops | Out-Null
if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }Why WriteAllText with UTF8Encoding($false)?
| Write method | Adds CRLF? | Adds BOM? | Safe for Linux SOPS? |
|---|---|---|---|
> (PS redirection) | ✅ | sometimes | ❌ SOPS timestamp parse fails (\x0d) |
Out-File | ✅ | ✅ (utf8 mode) | ❌ |
Set-Content | ✅ | depends | ❌ |
WriteAllText(path, content, UTF8Encoding($false)) | ❌ | ❌ | ✅ |
SOPS files are decrypted on Linux workers (bms-4, vps-i1). LF-only + no BOM is required.
Recovery from corruption
If canary fails after a write: docs/playbooks/sops-windows-crlf.md