Windows SOPS Write Patterns — Canonical Reference

Single source of truth for SOPS file operations on Windows.
Anything that writes or modifies a secrets/*.env.sops file 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 methodAdds CRLF?Adds BOM?Safe for Linux SOPS?
> (PS redirection)sometimes❌ SOPS timestamp parse fails (\x0d)
Out-File✅ (utf8 mode)
Set-Contentdepends
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