Playbook: Env Locking — Distributed Lease for SOPS and Server Operations
Table: p24_env_locks (Supabase)
Helper: scripts/env-lock.ps1
Last updated: 2026-07-07
Why this exists
Up to 16 concurrent Claude Code sessions run simultaneously on the developer workstation. Multiple sessions performing SOPS writes simultaneously corrupt the target file:
Session A: sops decrypt → modify GRAFANA_PASS → write plaintext → sops encrypt --in-place
Session B: sops decrypt → modify SMTP_PASS → write plaintext → sops encrypt --in-place
↑
B encrypts a file A is still writing.
One set of changes is lost or the file is unparseable.
p24_env_locks is a distributed lease table in Supabase — the only backend reachable from every
actor in the stack: 16 local sessions, CF Worker meta-dispatcher, and queue workers on bms-4/vps-i1.
Resource naming convention
| Resource string | Protects |
|---|---|
sops:monitoring | secrets/monitoring.env.sops |
sops:n8n-bms4 | secrets/n8n-bms4.env.sops |
sops:bms-servers | secrets/bms-servers.env.sops |
sops:administration | secrets/administration.env.sops |
sops:<stem> | Any secrets/<stem>.env.sops file |
server:bms-4 | bms-4 undergoing restart / config change |
server:vps-i1 | vps-i1 monitoring stack |
container:monitoring-stack | docker compose up/down on monitoring stack |
container:n8n-bms4 | n8n stack restart |
Use the file stem (no path, no extension) for SOPS resources.
Quick reference — PowerShell
# Dot-source the helper (once per session)
. C:\code_2026\p24-infra\scripts\env-lock.ps1
# Acquire before SOPS write
Acquire-EnvLock -Resource "sops:monitoring" -Operation "credential-rotation"
# ... do the SOPS work ...
# Release after canary decrypt confirms success
Release-EnvLock -Resource "sops:monitoring"
# List all active locks
Get-EnvLocks
# Extend TTL for long operations (call every 60s)
Update-EnvLockHeartbeat -Resource "sops:monitoring"Session identity ($script:P24_SESSION_ID) is a UUID persisted to a temp state file keyed by the
parent process id (%TEMP%\p24-env-lock-session-<parentPid>.id). Each Claude Code PowerShell tool
call is a separate process, so the id is read back from that file rather than regenerated —
meaning Acquire-EnvLock in one tool call and Release-EnvLock in a later call share the same
holder and the release actually deletes the row (#3844). No -SessionId arg is needed in normal
use. Set $env:P24_ENV_LOCK_SESSION_ID to pin an explicit identity across processes if needed.
What to do when a lock is held by another session
-
Check active locks:
Get-EnvLocksOutput shows Resource, Holder (first 8 chars of UUID), Operation, Acquired, Expires, Expired.
-
Check if holder session is alive:
# Query claude_sessions for the full holder UUID # (Get-EnvLocks shows first 8 chars — match against claude_sessions in Supabase dashboard # or via REST: GET /rest/v1/claude_sessions?session_id=eq.<full-uuid>) -
If holder session is dead (no heartbeat in claude_sessions, or Expired=YES):
# Force-release the stale lock (Acquire-EnvLock also does this automatically) Release-EnvLock -Resource "sops:monitoring" -SessionId "<full-holder-uuid>" # Then acquire normally Acquire-EnvLock -Resource "sops:monitoring" -Operation "your-operation" -
If holder session is alive — wait. Do not force-release a live session’s lock. Ping the other window / session to release when done.
TTL and heartbeat
-
Default TTL: 30 minutes. If your SOPS operation takes longer (unlikely), call:
Update-EnvLockHeartbeat -Resource "sops:monitoring"each 60 seconds to extend by another 30 minutes.
-
On agent crash: the lock auto-expires after TTL. The next
Acquire-EnvLockcall detectsexpires_at < now(), force-releases the stale row, and acquires cleanly.
Lock scope — when to use, when NOT to use
Use a lock for:
- Any SOPS file write (
sops encrypt --in-place) — mandatory gh workflow run secrets-sync.ymltriggers — recommended (prevents double-sync)docker compose restarton a production stack — recommended- Any SSH command that modifies a server’s
.envfile
Do NOT lock for:
- SOPS reads/decrypts — reads are safe to parallelise
- Git operations (checkout, commit, push)
- Local file edits that don’t touch SOPS
Integration with secret-manager sessions
The secret-manager.md startup sequence requires acquiring a lock before step 1:
. C:\code_2026\p24-infra\scripts\env-lock.ps1
Acquire-EnvLock -Resource "sops:<target-file-stem>" -Operation "secret-manager-<task>"Release at the end of the session, after canary decrypt confirms the SOPS file is intact:
sops --decrypt --input-type dotenv --output-type dotenv secrets\<file>.env.sops | Out-Null
if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — investigate before releasing lock" }
Release-EnvLock -Resource "sops:<target-file-stem>"Architecture notes
- Lock store is Supabase
p24_env_locks— accessible from all actors (CF Worker, local sessions, VPS workers) - Acquire is atomic at Postgres level:
INSERT ON CONFLICT— no race conditions - Redis on bms-4 and bms-1 are NOT used — both are application-level, inaccessible from CF Worker
- No cron needed for cleanup — TTL expiry is enforced at acquire time
- Grafana has
SELECTonp24_env_locks— add a panel to the ops dashboard if lock contention becomes frequent
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
BLOCKED: sops:monitoring is held by... | Another live session holds the lock | Wait, or check Get-EnvLocks + claude_sessions |
SOPS corrupt after canary | Lock was NOT held during write — another session interfered | Restore from git, rewrite, acquire lock this time |
SUPABASE_URL or SUPABASE_SERVICE_KEY not found | SOPS decrypt of monitoring.env.sops failed | Check $env:SOPS_AGE_KEY_FILE, run canary manually |
Lock shows Expired=YES in Get-EnvLocks | Dead session, lock was not released | Run Acquire-EnvLock — it auto-clears stale rows |