Skip to content

Commit 537f356

Browse files
solomonneascodex
andauthored
fix(work): stop concurrent sessions from re-arming closeout (#395)
Co-authored-by: Codex <codex@openai.com>
1 parent d3efbe4 commit 537f356

2 files changed

Lines changed: 77 additions & 3 deletions

File tree

src/brigade/claude_hooks/runtime.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,11 +1504,30 @@ def repo_worktree_fingerprint(target: Path) -> str | None:
15041504
return localio.stable_hash(sorted(lines))
15051505

15061506

1507-
def _bash_write_detected(target: Path, baseline: object) -> bool:
1507+
def _bash_write_detected(
1508+
target: Path,
1509+
baseline: object,
1510+
*,
1511+
session_id: str,
1512+
started_at: object,
1513+
) -> bool:
15081514
if not isinstance(baseline, str) or not baseline:
15091515
return False
15101516
current = repo_worktree_fingerprint(target)
1511-
return current is not None and current != baseline
1517+
if current is None or current == baseline:
1518+
return False
1519+
started = localio.parse_iso_datetime(started_at)
1520+
if started is None:
1521+
return True
1522+
for other_state in iter_session_states(target, limit=MAX_RECENT_SESSION_STATES):
1523+
if other_state.get("session_id") == session_id:
1524+
continue
1525+
if other_state.get("write_observed") is not True or other_state.get("repo_fingerprint") != current:
1526+
continue
1527+
other_write = localio.parse_iso_datetime(other_state.get("last_write_at"))
1528+
if other_write is not None and other_write >= started:
1529+
return False
1530+
return True
15121531

15131532

15141533
def _session_fingerprint(session_id: str) -> str:
@@ -1570,6 +1589,9 @@ def _normalize_state(target: Path, session_id: str, payload: dict[str, Any] | No
15701589
pending_fp = payload.get("pending_bash_fingerprint")
15711590
if isinstance(pending_fp, str) and pending_fp:
15721591
normalized["pending_bash_fingerprint"] = pending_fp
1592+
pending_started = localio.parse_iso_datetime(payload.get("pending_bash_started_at"))
1593+
if pending_started is not None and pending_started <= now:
1594+
normalized["pending_bash_started_at"] = pending_started.isoformat()
15731595
denied = payload.get("verify_denied_count")
15741596
if isinstance(denied, int) and not isinstance(denied, bool) and denied >= 0:
15751597
normalized["verify_denied_count"] = denied
@@ -1662,6 +1684,7 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
16621684
baseline = repo_worktree_fingerprint(target)
16631685
if baseline is not None:
16641686
state["pending_bash_fingerprint"] = baseline
1687+
state["pending_bash_started_at"] = localio.utc_now_iso()
16651688
write_session_state(target, session_id, state)
16661689
if not is_raw_verification(command):
16671690
return None
@@ -1699,7 +1722,12 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
16991722
tool_name == "Bash" and _is_confident_bash_write(post_tool_input.get("command"))
17001723
)
17011724
if not wrote and tool_name == "Bash":
1702-
wrote = _bash_write_detected(target, state.get("pending_bash_fingerprint"))
1725+
wrote = _bash_write_detected(
1726+
target,
1727+
state.get("pending_bash_fingerprint"),
1728+
session_id=session_id,
1729+
started_at=state.get("pending_bash_started_at"),
1730+
)
17031731
if wrote:
17041732
state["write_observed"] = True
17051733
written_at = localio.utc_now_iso()
@@ -1713,9 +1741,11 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
17131741
if updated_fp is not None:
17141742
state["repo_fingerprint"] = updated_fp
17151743
state.pop("pending_bash_fingerprint", None)
1744+
state.pop("pending_bash_started_at", None)
17161745
write_session_state(target, session_id, state)
17171746
elif state.get("pending_bash_fingerprint") is not None:
17181747
state.pop("pending_bash_fingerprint", None)
1748+
state.pop("pending_bash_started_at", None)
17191749
write_session_state(target, session_id, state)
17201750
return None
17211751

tests/test_claude_hooks_runtime.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,50 @@ def test_posttooluse_records_python_c_write_via_repo_snapshot(tmp_path: Path):
368368
assert blocked["decision"] == "block"
369369

370370

371+
def test_posttooluse_ignores_concurrent_session_write_during_read_only_bash(tmp_path: Path):
372+
target = _wired_claude(tmp_path)
373+
reader_session = "read-only-bash"
374+
writer_session = "concurrent-writer"
375+
pretool = _payload(
376+
target,
377+
"PreToolUse",
378+
session_id=reader_session,
379+
tool_name="Bash",
380+
tool_input={"command": f"{sys.executable} -c \"print('noop')\""},
381+
)
382+
assert runtime.handle_payload("PreToolUse", pretool) is None
383+
pending_reader_state = runtime.read_session_state(target, reader_session)
384+
assert "pending_bash_fingerprint" in pending_reader_state
385+
assert "pending_bash_started_at" in pending_reader_state
386+
387+
changed = target / "concurrent.py"
388+
changed.write_text("changed\n")
389+
assert (
390+
runtime.handle_payload(
391+
"PostToolUse",
392+
_payload(
393+
target,
394+
"PostToolUse",
395+
session_id=writer_session,
396+
tool_name="Write",
397+
tool_input={"file_path": str(changed)},
398+
),
399+
)
400+
is None
401+
)
402+
assert runtime.read_session_state(target, writer_session)["write_observed"] is True
403+
404+
assert runtime.handle_payload("PostToolUse", {**pretool, "hook_event_name": "PostToolUse"}) is None
405+
reader_state = runtime.read_session_state(target, reader_session)
406+
assert reader_state["write_observed"] is False
407+
assert "pending_bash_fingerprint" not in reader_state
408+
assert "pending_bash_started_at" not in reader_state
409+
assert (
410+
runtime.handle_payload("Stop", _payload(target, "Stop", session_id=reader_session, stop_hook_active=False))
411+
is None
412+
)
413+
414+
371415
def test_posttooluse_snapshot_fails_open_when_state_cannot_be_inspected(tmp_path: Path, monkeypatch):
372416
target = _wired_claude(tmp_path)
373417
session_id = "snapshot-unavailable"

0 commit comments

Comments
 (0)