Skip to content

fix(guard): shared lock for guards.json write-race - #288

Merged
Data-Wise merged 6 commits into
devfrom
feature/guards-json-lock
Jul 15, 2026
Merged

fix(guard): shared lock for guards.json write-race#288
Data-Wise merged 6 commits into
devfrom
feature/guards-json-lock

Conversation

@Data-Wise

Copy link
Copy Markdown
Owner

Summary

PR B of SPEC-guard-hardening-adversarial-review-2026-07-15.md. Closes a confirmed lost-update race in ~/.claude/guards.json's mutation path.

  • Empirically verified this session: a same-machine test lost 40/80 concurrent writes under the documented jq ... > tmp && mv tmp file pattern with no locking.
  • Adversarial re-check after the fix was scoped found a second, previously-uncovered writer: scripts/install-guards.sh's seed/merge path uses the identical unsafe pattern — skills/dev/git/SKILL.md Operation 12's "sole sanctioned mutator" claim was not accurate.

Changes

  • New lib/guards-lock.sh — mkdir-based lock (atomic on both macOS and Linux; flock has no CLI on macOS, confirmed) with a staleness timeout so a crashed caller can't wedge the lock permanently.
  • Wired into both writers: skills/dev/git/SKILL.md Operation 12 (enable/disable/profile) and scripts/install-guards.sh (fresh-create + per-guard merge loop, each iteration independently lock-guarded so a concurrent Operation 12 mutation isn't blocked for the full loop).
  • New tests/test_guards_registry_concurrency.sh — 3 cases, all against a mktemp -d scratch copy (hard assert_not_real guard, never the real ~/.claude/guards.json): concurrent-writer race, cross-writer (Operation 12 + install-guards.sh) shared-lock proof, stale-lock-timeout recovery.
  • skills/dev/git/SKILL.md — corrected the "sole sanctioned mutator" claim to reflect the shared lock and both writers.
  • CHANGELOG (CHANGELOG.md + docs/CHANGELOG.md) [Unreleased] entries.

Test plan

  • tests/test_guards_registry_concurrency.sh — 7/7, red-first verified: MODE=unlocked reliably reproduces lost updates (e.g. 17/40+23/40, never 40/40+40/40), MODE=locked (default) always lands 40/40+40/40 across repeated runs
  • Full suite: python3 -m pytest tests/ — 1678 passed, 33 skipped, 1 xpassed, 1 failed (pre-existing, environmental — same ambient guards.json mute unrelated to this branch, confirmed by inspection)
  • One incidental bug found and fixed along the way: a spurious "stale lock" warning on a normal release race (lock dir vanishing between the existence check and stat, not an actual crash)
  • Independently re-verified outside the implementing agent's own report — first completion notification showed Phase 5 files modified-but-uncommitted despite checkboxes marked done (caught via the ORCHESTRATE hang-detection cross-check); resumed the same agent via SendMessage rather than a fresh dispatch, which committed cleanly

🤖 Generated with Claude Code

Data-Wise and others added 6 commits July 15, 2026 11:11
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 1 of the guards.json write-race fix. mkdir is atomic on both
macOS and Linux (flock has no CLI on macOS, only the syscall), so it's
used as the lock primitive with a staleness timeout so a crashed
holder can't wedge the lock permanently.

Standalone script (lib/guards-lock.sh), not a sourced function --
Operation 12's guards.json mutation is inline bash issued per-turn by
the LLM with no backing script, so it can't share a shell process with
install-guards.sh's separate invocation.
Phase 2 of the guards.json write-race fix. enable/disable/profile's
jq-mutate step and the mute-expiry auto-sweep (when it actually clears
a mute) now acquire lib/guards-lock.sh before the read-modify-write
and release immediately after, with a trap EXIT cleanup so a mid-write
failure can't leak the lock.
Phase 3 of the guards.json write-race fix -- the second writer found
during adversarial review. Both the fresh-create branch and the
per-guard merge loop now acquire lib/guards-lock.sh before their
read-modify-write and release immediately after, with trap EXIT
cleanup. The merge loop keeps a separate acquire/release per guard
iteration rather than one lock held across the whole loop, so a
concurrent Operation 12 mutation isn't blocked for the full duration.
Verified against a scratch HOME (fresh-create + idempotent re-run).
Phase 4 of the guards.json write-race fix. Formalizes this session's
ad hoc falsification test (2 concurrent jq-mutate loops, 40 writes
each, scratch copy) as tests/test_guards_registry_concurrency.sh:

- Case 1: two concurrent writers, distinct fields -- zero lost updates.
- Case 2: an Operation-12-style writer concurrent with an
  install-guards.sh-style merge writer against the same scratch file --
  proves the shared lock, not just per-writer self-consistency.
- Case 3: a pre-staged stale lock dir is force-broken within the
  timeout rather than hanging.

Every case runs against a mktemp -d scratch copy; a hard
assert_not_real guard aborts the suite if any path ever matches the
real ~/.claude/guards.json. Red-first verified: MODE=unlocked
reproduces lost updates on the pre-fix code path across repeated
runs; MODE=locked (default) passes 7/7 with zero lost updates.

Also fixes a bug found while red-lining the suite: lib/guards-lock.sh
logged a spurious "breaking stale lock" warning when a lock dir
vanished between its existence check and its stat call (the holder
releasing normally, not a crash) -- now retries immediately instead
of misreporting staleness.
Phase 5 of the guards.json write-race fix. skills/dev/git/SKILL.md
Operation 12 no longer claims to be the sole sanctioned mutator of
guards.json -- corrected to name both writers (Operation 12 and
install-guards.sh) and the shared lib/guards-lock.sh lock that now
coordinates them, per the second-writer finding from adversarial
review. Mirrored [Unreleased] Fixed entries added to CHANGELOG.md and
docs/CHANGELOG.md.
@Data-Wise
Data-Wise merged commit d149bcf into dev Jul 15, 2026
6 checks passed
Data-Wise added a commit that referenced this pull request Jul 15, 2026
All three guard-hardening PRs merged to dev with green CI:
#287 (cd-target resolution), #288 (guards.json lock, no CHANGELOG
conflict with #287 despite both touching it), #289 (orchestrate-dispatch
hardening). Worktrees removed, local branches deleted via safe `-d`
(squash-merge content recognized without needing force).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Data-Wise added a commit that referenced this pull request Jul 15, 2026
ORCHESTRATE-*.md files belong on feature branches during development,
not on dev after merge (missed during the #287/#288 squash-merge cleanup).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Data-Wise added a commit that referenced this pull request Jul 16, 2026
Three claims in GRILL-branch-guard-target-resolution-2026-07-14.md are
now stale given this session's guard-hardening work: the disproven
no-switch-guard "compound-string-match/worktree-cleanup" framing, the
concurrency-safety item marked deferred (now fixed in #288, broader
than originally scoped), and the cd/-C cumulative-tracking question
marked unresolved (now locked and shipped in #287). Added forward
pointers to the actual resolutions rather than leaving a future
session to re-derive them from a stale doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Data-Wise
Data-Wise deleted the feature/guards-json-lock branch July 24, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant