@@ -1113,26 +1113,24 @@ fn default_read_only_subpaths_for_writable_root(
11131113 // (file .git with gitdir pointer), and bare repos when the gitdir is the
11141114 // writable root itself.
11151115 let top_level_git_is_file = top_level_git. as_path ( ) . is_file ( ) ;
1116- let top_level_git_is_dir = top_level_git. as_path ( ) . is_dir ( ) ;
1117- if top_level_git_is_dir || top_level_git_is_file {
1118- if top_level_git_is_file
1119- && is_git_pointer_file ( & top_level_git)
1120- && let Some ( gitdir) = resolve_gitdir_from_file ( & top_level_git)
1121- {
1122- subpaths. push ( gitdir) ;
1123- }
1124- subpaths. push ( top_level_git) ;
1116+ if top_level_git_is_file
1117+ && is_git_pointer_file ( & top_level_git)
1118+ && let Some ( gitdir) = resolve_gitdir_from_file ( & top_level_git)
1119+ {
1120+ subpaths. push ( gitdir) ;
11251121 }
1122+ subpaths. push ( top_level_git) ;
11261123
11271124 let top_level_agents = writable_root. join ( ".agents" ) ;
11281125 if top_level_agents. as_path ( ) . is_dir ( ) {
11291126 subpaths. push ( top_level_agents) ;
11301127 }
11311128
1132- // Keep top-level project metadata under .codex read-only to the agent by
1133- // default. For the workspace root itself, protect it even before the
1134- // directory exists so first-time creation still goes through the
1135- // protected-path approval flow.
1129+ // Keep top level project metadata under .git and .codex read-only to the
1130+ // agent by default. Existing .git pointer files still protect their
1131+ // resolved gitdir targets when present. For the workspace root itself,
1132+ // protect .codex even before it exists so first time creation still goes
1133+ // through the protected path approval flow.
11361134 let top_level_codex = writable_root. join ( ".codex" ) ;
11371135 if protect_missing_dot_codex || top_level_codex. as_path ( ) . is_dir ( ) {
11381136 subpaths. push ( top_level_codex) ;
@@ -1290,12 +1288,13 @@ mod tests {
12901288
12911289 #[ cfg( unix) ]
12921290 #[ test]
1293- fn writable_roots_proactively_protect_missing_dot_codex ( ) {
1291+ fn writable_roots_reserve_missing_project_metadata_paths ( ) {
12941292 let cwd = TempDir :: new ( ) . expect ( "tempdir" ) ;
12951293 let expected_root = AbsolutePathBuf :: from_absolute_path (
12961294 cwd. path ( ) . canonicalize ( ) . expect ( "canonicalize cwd" ) ,
12971295 )
12981296 . expect ( "absolute canonical root" ) ;
1297+ let expected_dot_git = expected_root. join ( ".git" ) ;
12991298 let expected_dot_codex = expected_root. join ( ".codex" ) ;
13001299
13011300 let policy = FileSystemSandboxPolicy :: restricted ( vec ! [ FileSystemSandboxEntry {
@@ -1308,6 +1307,11 @@ mod tests {
13081307 let writable_roots = policy. get_writable_roots_with_cwd ( cwd. path ( ) ) ;
13091308 assert_eq ! ( writable_roots. len( ) , 1 ) ;
13101309 assert_eq ! ( writable_roots[ 0 ] . root, expected_root) ;
1310+ assert ! (
1311+ writable_roots[ 0 ]
1312+ . read_only_subpaths
1313+ . contains( & expected_dot_git)
1314+ ) ;
13111315 assert ! (
13121316 writable_roots[ 0 ]
13131317 . read_only_subpaths
@@ -1383,6 +1387,13 @@ mod tests {
13831387 #[ test]
13841388 fn legacy_workspace_write_projection_accepts_relative_cwd ( ) {
13851389 let relative_cwd = Path :: new ( "workspace" ) ;
1390+ let expected_dot_git = AbsolutePathBuf :: from_absolute_path (
1391+ std:: env:: current_dir ( )
1392+ . expect ( "current dir" )
1393+ . join ( relative_cwd)
1394+ . join ( ".git" ) ,
1395+ )
1396+ . expect ( "absolute dot git" ) ;
13861397 let expected_dot_codex = AbsolutePathBuf :: from_absolute_path (
13871398 std:: env:: current_dir ( )
13881399 . expect ( "current dir" )
@@ -1413,6 +1424,12 @@ mod tests {
14131424 } ,
14141425 access: FileSystemAccessMode :: Write ,
14151426 } ,
1427+ FileSystemSandboxEntry {
1428+ path: FileSystemPath :: Path {
1429+ path: expected_dot_git,
1430+ } ,
1431+ access: FileSystemAccessMode :: Read ,
1432+ } ,
14161433 FileSystemSandboxEntry {
14171434 path: FileSystemPath :: Path {
14181435 path: expected_dot_codex,
@@ -1498,6 +1515,7 @@ mod tests {
14981515 let expected_root =
14991516 AbsolutePathBuf :: from_absolute_path ( & link_root) . expect ( "absolute symlinked root" ) ;
15001517 let expected_blocked = link_blocked. clone ( ) ;
1518+ let expected_git = expected_root. join ( ".git" ) ;
15011519 let expected_agents = expected_root. join ( ".agents" ) ;
15021520 let expected_codex = expected_root. join ( ".codex" ) ;
15031521
@@ -1542,6 +1560,7 @@ mod tests {
15421560 . read_only_subpaths
15431561 . contains( & expected_agents)
15441562 ) ;
1563+ assert ! ( writable_roots[ 0 ] . read_only_subpaths. contains( & expected_git) ) ;
15451564 assert ! (
15461565 writable_roots[ 0 ]
15471566 . read_only_subpaths
@@ -1560,6 +1579,13 @@ mod tests {
15601579 symlink_dir ( & decoy, & dot_codex) . expect ( "create .codex symlink" ) ;
15611580
15621581 let root = AbsolutePathBuf :: from_absolute_path ( & root) . expect ( "absolute root" ) ;
1582+ let expected_dot_git = AbsolutePathBuf :: from_absolute_path (
1583+ root. as_path ( )
1584+ . canonicalize ( )
1585+ . expect ( "canonicalize root" )
1586+ . join ( ".git" ) ,
1587+ )
1588+ . expect ( "absolute .git path" ) ;
15631589 let expected_dot_codex = AbsolutePathBuf :: from_absolute_path (
15641590 root. as_path ( )
15651591 . canonicalize ( )
@@ -1580,7 +1606,7 @@ mod tests {
15801606 assert_eq ! ( writable_roots. len( ) , 1 ) ;
15811607 assert_eq ! (
15821608 writable_roots[ 0 ] . read_only_subpaths,
1583- vec![ expected_dot_codex]
1609+ vec![ expected_dot_git , expected_dot_codex]
15841610 ) ;
15851611 assert ! (
15861612 !writable_roots[ 0 ]
@@ -1626,7 +1652,7 @@ mod tests {
16261652 assert_eq ! ( writable_roots[ 0 ] . root, expected_root) ;
16271653 assert_eq ! (
16281654 writable_roots[ 0 ] . read_only_subpaths,
1629- vec![ expected_linked_private]
1655+ vec![ expected_root . join ( ".git" ) , expected_linked_private]
16301656 ) ;
16311657 assert ! (
16321658 !writable_roots[ 0 ]
@@ -1673,7 +1699,7 @@ mod tests {
16731699 assert_eq ! ( writable_roots[ 0 ] . root, expected_root) ;
16741700 assert_eq ! (
16751701 writable_roots[ 0 ] . read_only_subpaths,
1676- vec![ expected_linked_private]
1702+ vec![ expected_root . join ( ".git" ) , expected_linked_private]
16771703 ) ;
16781704 assert ! (
16791705 !writable_roots[ 0 ]
@@ -1713,7 +1739,10 @@ mod tests {
17131739 let writable_roots = policy. get_writable_roots_with_cwd ( cwd. path ( ) ) ;
17141740 assert_eq ! ( writable_roots. len( ) , 1 ) ;
17151741 assert_eq ! ( writable_roots[ 0 ] . root, expected_root) ;
1716- assert_eq ! ( writable_roots[ 0 ] . read_only_subpaths, vec![ expected_alias] ) ;
1742+ assert_eq ! (
1743+ writable_roots[ 0 ] . read_only_subpaths,
1744+ vec![ expected_root. join( ".git" ) , expected_alias]
1745+ ) ;
17171746 }
17181747
17191748 #[ cfg( unix) ]
@@ -1751,6 +1780,7 @@ mod tests {
17511780 let expected_root =
17521781 AbsolutePathBuf :: from_absolute_path ( & link_tmpdir) . expect ( "absolute symlinked tmpdir" ) ;
17531782 let expected_blocked = link_blocked. clone ( ) ;
1783+ let expected_git = expected_root. join ( ".git" ) ;
17541784 let expected_codex = expected_root. join ( ".codex" ) ;
17551785
17561786 unsafe {
@@ -1783,6 +1813,7 @@ mod tests {
17831813 . read_only_subpaths
17841814 . contains( & expected_blocked)
17851815 ) ;
1816+ assert ! ( writable_roots[ 0 ] . read_only_subpaths. contains( & expected_git) ) ;
17861817 assert ! (
17871818 writable_roots[ 0 ]
17881819 . read_only_subpaths
0 commit comments