Skip to content

Runbook — Locking shared writable state (.env, git worktrees) under /opt

Use when: you are about to edit a shared .env file, or run a multi-step git transaction (add + commit + push) on a repo working tree under /opt that more than one Claude Code session can touch — i.e. anything outside a per-session git worktree.

Why this exists

Multiple Claude Code sessions can run concurrently against the same VPS, and several /opt/dev/* and /opt/*-compose/ trees are shared, non-worktree working directories with one .git/.env between all sessions.

A single git invocation locks its own .git/index.lock, but a sequence of invocations is not atomic. On 2026-07-07 two sessions hit /opt/dev/jb-brain at the same time: origin/main moved underneath one session mid-commit, a sibling git reset cleared the other session's staged index, and both wrote .gitignore over each other. It converged by luck that time — no lost commit, no leaked secret — but the same race can just as easily land a bad commit or a secret in history. The same day, #937 and #1158 raced on a shared .env. Task #1175 tracked the fix. See [[feedback_shared_git_worktree_race]] in memory.

The fix: with-lock.sh

/opt/infrastructure/scripts/with-lock.sh holds an exclusive flock for the entire wrapped command, keyed by a hash of the resource path. Any two sessions naming the same resource path serialize against each other automatically — no shared session state needed beyond the filesystem.

with-lock.sh <resource-path> [--timeout SECONDS] -- <command...>
  • resource-path — any stable identifier for the thing being protected. Use the repo root for git operations, the file path for .env edits. It does not need to exist; it's only hashed to pick a lock file.
  • --timeout — seconds to wait for the lock before giving up (default 60).
  • Exit codes: 0 success (wrapped command's own exit code is returned), 124 lock not acquired within timeout (resource busy — do not retry in a loop, back off and tell the user), 2 usage error.

Wrapping a git transaction

with-lock.sh /opt/dev/jb-brain -- bash -c '
  cd /opt/dev/jb-brain &&
  git add -A &&
  git commit -m "fix: ..." &&
  git push origin main
'

Wrapping a .env edit

with-lock.sh /opt/rabbithall-compose/.env -- bash -c '
  cp /opt/rabbithall-compose/.env /opt/rabbithall-compose/.env.bak-$(date -u +%Y%m%d%H%M%S) &&
  sed -i "s/^SOME_KEY=.*/SOME_KEY=newvalue/" /opt/rabbithall-compose/.env
'

When you can skip this

  • Any repo already checked out into a dedicated git worktree for your session (git worktree add) — no other session shares that working tree.
  • Read-only operations (git status, git log, cat .env).

Troubleshooting

Lock times out (exit 124)

Another session is mid-transaction on the same resource. Do not force past it (rm the lock file, --timeout 0, etc.) — that reintroduces the exact race this exists to prevent. Wait and retry, or tell the user the resource is busy.

Stale lock after a crashed session

flock releases automatically when the holding process exits or dies — there is no cleanup step needed. The /opt/infrastructure/locks/*.lock files are harmless to leave in place; they are re-used by path hash, not recreated.

Not yet covered

This wrapper is opt-in per invocation — it does not intercept bare git/vi calls that bypass it. Enforcing it automatically (e.g. via a git wrapper alias on PATH) was considered and rejected for now: it would silently change behavior for tooling that doesn't expect to be wrapped. Current mitigation is this runbook + the ticket note on #1175 — every session working on a shared /opt tree or .env is expected to use it.