@@ -412,7 +412,7 @@ def test_posttooluse_ignores_concurrent_session_write_during_read_only_bash(tmp_
412412 )
413413
414414
415- def test_posttooluse_snapshot_fails_open_when_state_cannot_be_inspected (tmp_path : Path , monkeypatch ):
415+ def test_posttooluse_snapshot_fails_closed_when_state_cannot_be_inspected (tmp_path : Path , monkeypatch ):
416416 target = _wired_claude (tmp_path )
417417 session_id = "snapshot-unavailable"
418418 monkeypatch .setattr (runtime , "repo_worktree_fingerprint" , lambda repo : None )
@@ -424,11 +424,61 @@ def test_posttooluse_snapshot_fails_open_when_state_cannot_be_inspected(tmp_path
424424 tool_input = {"command" : f"{ sys .executable } -c \" print('noop')\" " },
425425 )
426426 assert runtime .handle_payload ("PreToolUse" , pretool ) is None
427- assert runtime .read_session_state (target , session_id ).get ("pending_bash_fingerprint" ) is None
427+ assert runtime .read_session_state (target , session_id ).get ("pending_bash_fingerprint" ) == "unavailable"
428428
429429 succeeded = {** pretool , "hook_event_name" : "PostToolUse" }
430430 assert runtime .handle_payload ("PostToolUse" , succeeded ) is None
431- assert runtime .read_session_state (target , session_id )["write_observed" ] is False
431+ assert runtime .read_session_state (target , session_id )["write_observed" ] is True
432+ blocked = runtime .handle_payload ("Stop" , _payload (target , "Stop" , session_id = session_id , stop_hook_active = False ))
433+ assert blocked ["decision" ] == "block"
434+
435+
436+ def test_posttooluse_fails_closed_when_post_command_snapshot_is_unavailable (tmp_path : Path , monkeypatch ):
437+ target = _wired_claude (tmp_path )
438+ session_id = "post-snapshot-unavailable"
439+ fingerprint_calls = 0
440+
441+ def sequenced_fingerprint (repo : Path ) -> str | None :
442+ nonlocal fingerprint_calls
443+ fingerprint_calls += 1
444+ return "baseline" if fingerprint_calls < 4 else None
445+
446+ monkeypatch .setattr (runtime , "repo_worktree_fingerprint" , sequenced_fingerprint )
447+ pretool = _payload (
448+ target ,
449+ "PreToolUse" ,
450+ session_id = session_id ,
451+ tool_name = "Bash" ,
452+ tool_input = {"command" : f"{ sys .executable } -c \" print('noop')\" " },
453+ )
454+
455+ assert runtime .handle_payload ("PreToolUse" , pretool ) is None
456+ assert runtime .read_session_state (target , session_id )["pending_bash_fingerprint" ] == "baseline"
457+ assert runtime .handle_payload ("PostToolUse" , {** pretool , "hook_event_name" : "PostToolUse" }) is None
458+
459+ assert runtime .read_session_state (target , session_id )["write_observed" ] is True
460+ blocked = runtime .handle_payload ("Stop" , _payload (target , "Stop" , session_id = session_id , stop_hook_active = False ))
461+ assert blocked ["decision" ] == "block"
462+
463+
464+ @pytest .mark .parametrize ("command" , ["gh --version" , "jq --version" , "rg --version" ])
465+ def test_posttooluse_unlisted_read_only_command_does_not_observe_write (tmp_path : Path , command : str ):
466+ target = _git_wired_claude (tmp_path )
467+ session_id = f"read-only-{ command .split ()[0 ]} "
468+ pretool = _payload (
469+ target ,
470+ "PreToolUse" ,
471+ session_id = session_id ,
472+ tool_name = "Bash" ,
473+ tool_input = {"command" : command },
474+ )
475+
476+ assert runtime .handle_payload ("PreToolUse" , pretool ) is None
477+ assert runtime .handle_payload ("PostToolUse" , {** pretool , "hook_event_name" : "PostToolUse" }) is None
478+
479+ state = runtime .read_session_state (target , session_id )
480+ assert state ["write_observed" ] is False
481+ assert "pending_bash_fingerprint" not in state
432482 assert (
433483 runtime .handle_payload ("Stop" , _payload (target , "Stop" , session_id = session_id , stop_hook_active = False )) is None
434484 )
@@ -840,7 +890,7 @@ def fake_run(repo: Path, *git_args: str):
840890 assert runtime .repo_worktree_fingerprint (target ) is None
841891
842892
843- def test_posttooluse_does_not_record_bash_write_when_hash_object_fails_for_untracked (tmp_path : Path , monkeypatch ):
893+ def test_posttooluse_fails_closed_when_untracked_state_check_fails (tmp_path : Path , monkeypatch ):
844894 target = _git_wired_claude (tmp_path )
845895 session_id = "hash-object-fail"
846896 out_file = target / "new.txt"
@@ -864,15 +914,14 @@ def fake_run(repo: Path, *git_args: str):
864914 assert runtime .handle_payload ("PreToolUse" , pretool ) is None
865915 state = runtime .read_session_state (target , session_id )
866916 assert state ["write_observed" ] is False
867- assert "pending_bash_fingerprint" not in state
917+ assert state [ "pending_bash_fingerprint" ] == "unavailable"
868918
869919 out_file .write_text ("after" )
870920 succeeded = {** pretool , "hook_event_name" : "PostToolUse" }
871921 assert runtime .handle_payload ("PostToolUse" , succeeded ) is None
872- assert runtime .read_session_state (target , session_id )["write_observed" ] is False
873- assert (
874- runtime .handle_payload ("Stop" , _payload (target , "Stop" , session_id = session_id , stop_hook_active = False )) is None
875- )
922+ assert runtime .read_session_state (target , session_id )["write_observed" ] is True
923+ blocked = runtime .handle_payload ("Stop" , _payload (target , "Stop" , session_id = session_id , stop_hook_active = False ))
924+ assert blocked ["decision" ] == "block"
876925
877926
878927def test_posttooluse_records_bash_write_on_dirty_tracked_same_size_rewrite (tmp_path : Path ):
0 commit comments