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 stringProtects
sops:monitoringsecrets/monitoring.env.sops
sops:n8n-bms4secrets/n8n-bms4.env.sops
sops:bms-serverssecrets/bms-servers.env.sops
sops:administrationsecrets/administration.env.sops
sops:<stem>Any secrets/<stem>.env.sops file
server:bms-4bms-4 undergoing restart / config change
server:vps-i1vps-i1 monitoring stack
container:monitoring-stackdocker compose up/down on monitoring stack
container:n8n-bms4n8n 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

  1. Check active locks:

    Get-EnvLocks

    Output shows Resource, Holder (first 8 chars of UUID), Operation, Acquired, Expires, Expired.

  2. 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>)
  3. 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"
  4. 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-EnvLock call detects expires_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.yml triggers — recommended (prevents double-sync)
  • docker compose restart on a production stack — recommended
  • Any SSH command that modifies a server’s .env file

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 SELECT on p24_env_locks — add a panel to the ops dashboard if lock contention becomes frequent

Troubleshooting

SymptomCauseFix
BLOCKED: sops:monitoring is held by...Another live session holds the lockWait, or check Get-EnvLocks + claude_sessions
SOPS corrupt after canaryLock was NOT held during write — another session interferedRestore from git, rewrite, acquire lock this time
SUPABASE_URL or SUPABASE_SERVICE_KEY not foundSOPS decrypt of monitoring.env.sops failedCheck $env:SOPS_AGE_KEY_FILE, run canary manually
Lock shows Expired=YES in Get-EnvLocksDead session, lock was not releasedRun Acquire-EnvLock — it auto-clears stale rows