safe-chains 0.230.0

Auto-allow safe bash commands in agentic coding tools
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
//! HP-20 filesystem region model. A positive, structured classifier over paths โ€” the
//! read/write analogue of the command allowlist. `regions/default.toml` maps path shapes to
//! ROLES; each role projects to the `LocalLocus` ladder through two faces (a read face and a
//! write face) plus a `reads_secret` bit, so the same path read is safe / written is denied.
//!
//! Matching is most-specific-wins (exact > longer prefix > segment), OS-scoped to the running
//! platform, and fail-closed: an absolute/home path matching nothing is `unknown` (deny),
//! a bare relative path is `worktree`. Runs AFTER the `$VAR`/`..` guard in `locus.rs`.

use std::collections::HashMap;
use std::sync::LazyLock;

use serde::Deserialize;

use crate::engine::facet::{FacetTerm, LocalLocus};

/// Which faces a user grant may NOT widen.
///
/// Two distinct needs, and collapsing them was a real over-deny. safe-chains' own config must not
/// be WRITTEN at all, because any write to it decides what gets approved next. Its parent directory
/// is different: writing a file into `~/.config` is ordinary, and what must not happen is the
/// directory being replaced by something pointing elsewhere.
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Frozen {
    /// A grant widens every face. The default.
    Nothing,
    /// A grant may not widen REBIND. Writing into the node is still grantable.
    Rebind,
    /// A grant may not widen WRITE, and therefore not REBIND either.
    Write,
}

/// A role's projection: the locus a READ reaches, the locus a WRITE reaches, the locus a REBIND
/// reaches, and whether reading it extracts a secret.
///
/// REBIND is the third face, and it answers a question the other two cannot: may this operation
/// change what the NAME refers to? `rm` removes the binding, `ln` points it somewhere else, `mv`
/// takes it away โ€” all rebinds. `cp`, `touch` and a redirect write THROUGH the name to the bytes
/// underneath, which is an ordinary write. The distinction cannot be read off the operation alone:
/// `cp x DIR` and `ln -s y DIR` are both `create`/`transfer` to the engine.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Role {
    pub read_locus: LocalLocus,
    pub write_locus: LocalLocus,
    /// Defaults to `write_locus`; only a grant, or an explicit `rebind_locus`, separates them.
    pub rebind_locus: LocalLocus,
    pub reads_secret: bool,
    pub frozen: Frozen,
}

impl Role {
    /// Whether a grant may widen this role's WRITE face.
    fn write_grantable(&self) -> bool {
        self.frozen != Frozen::Write && self.write_locus < LocalLocus::SystemIntegrity
    }

    /// Whether a grant may widen this role's REBIND face. Strictly stronger: freezing the write
    /// necessarily freezes the rebind, since a rebind is the more destructive of the two.
    fn rebind_grantable(&self) -> bool {
        self.frozen == Frozen::Nothing && self.write_locus < LocalLocus::SystemIntegrity
    }
}

#[derive(Deserialize)]
struct RegionsFile {
    #[serde(default)]
    role: HashMap<String, RoleDef>,
    #[serde(default)]
    region: Vec<RegionDef>,
}

#[derive(Deserialize)]
struct RoleDef {
    read_locus: String,
    write_locus: String,
    /// Absent โ†’ the write locus. Only a role that must be writable-into but not replaceable states
    /// it separately.
    #[serde(default)]
    rebind_locus: Option<String>,
    #[serde(default)]
    reads_secret: bool,
    /// `"rebind"` or `"write"`; absent โ†’ nothing is frozen.
    #[serde(default)]
    frozen: Option<String>,
    #[serde(default)]
    #[allow(dead_code)] // policy prose, not consumed by the classifier
    description: String,
}

#[derive(Deserialize)]
struct RegionDef {
    path: String,
    role: String,
    os: Option<Vec<String>>,
    // `note` / `researched` are dated provenance (mirrors `researched_version`) โ€” parsed so
    // the file validates, but not consumed by the classifier.
    #[serde(default)]
    #[allow(dead_code)]
    note: String,
    #[serde(default)]
    #[allow(dead_code)]
    researched: String,
}

enum Matcher {
    /// `/etc/hosts`, `~` โ€” the whole path (a `Prefix` also matches its own dir, sans slash).
    Exact(String),
    /// `/proc/sys/` โ€” the subtree under it (path is a descendant, or the dir itself).
    Prefix(String),
    /// `/dev/sd*` โ€” a raw string prefix (device families: matches `/dev/sda`, `/dev/sda1`).
    StringPrefix(String),
    /// `.git`, `.envrc` โ€” any path component equal to it, at any depth.
    Segment(String),
    /// `/proc/*/environ` โ€” `*` stands for exactly one path component; every other component is
    /// literal. For the entries that are per-PROCESS rather than per-host: `/proc/1234/environ`
    /// holds that process's environment, which is where an agent's API tokens live, and there is
    /// no way to name the pid ahead of time. A subtree node would have to swallow `/proc/cpuinfo`
    /// with it.
    Glob(Vec<String>),
}

impl Matcher {
    fn from_path(path: &str) -> Matcher {
        if let Some(p) = path.strip_suffix('*') {
            Matcher::StringPrefix(p.to_string())
        } else if path.contains('*') {
            Matcher::Glob(path.split('/').map(str::to_string).collect())
        } else if path.ends_with('/') {
            Matcher::Prefix(path.to_string())
        } else if path.starts_with('/') || path.starts_with('~') {
            Matcher::Exact(path.to_string())
        } else {
            Matcher::Segment(path.to_string())
        }
    }

    /// Specificity of a match against `path`, or `None` if it doesn't match. Higher = more
    /// specific: exact โ‰ซ any prefix โ‰ซ any segment, and within a kind, longer wins. When `fold`,
    /// comparisons are ASCII-case-insensitive โ€” used for DENY-shield nodes on a case-insensitive
    /// filesystem (macOS), so a case-variant spelling (`~/.AWS`, `.GIT/hooks`) can't evade a
    /// credential store or a write-freeze that, on that filesystem, names the very same file.
    fn specificity(&self, path: &str, fold: bool) -> Option<usize> {
        let eq = |a: &str, b: &str| if fold { a.eq_ignore_ascii_case(b) } else { a == b };
        let starts = |h: &str, p: &str| if fold { ci_starts_with(h, p) } else { h.starts_with(p) };
        match self {
            Matcher::Exact(s) => eq(path, s).then_some(1_000_000 + s.len()),
            Matcher::Prefix(s) => {
                let dir = s.strip_suffix('/').unwrap_or(s);
                (starts(path, s.as_str()) || eq(path, dir)).then_some(1_000 + s.len())
            }
            Matcher::StringPrefix(s) => starts(path, s.as_str()).then_some(1_000 + s.len()),
            Matcher::Segment(seg) => path.split('/').any(|c| eq(c, seg)).then_some(seg.len()),
            // Between Prefix and Exact: it pins every component but one, so it must outrank a
            // subtree node while still yielding to a node that spells the path out in full.
            Matcher::Glob(pat) => {
                let comps: Vec<&str> = path.split('/').collect();
                (comps.len() == pat.len()
                    && pat.iter().zip(&comps).all(|(p, c)| p == "*" || eq(c, p)))
                .then_some(10_000 + path.len())
            }
        }
    }

    /// The part of `path` below this matcher's root โ€” used to keep a grant from widening a
    /// HIDDEN (dot-prefixed) file or dir it swept up. A `~/` grant matches `~/.ssh` and
    /// `~/projects`, but only the latter's remainder is dot-free.
    fn remainder<'a>(&self, path: &'a str) -> &'a str {
        match self {
            Matcher::Prefix(s) | Matcher::StringPrefix(s) => path.strip_prefix(s.as_str()).unwrap_or(""),
            Matcher::Exact(_) => "",
            // A Glob pins every component, so like an Exact it matches the whole path: nothing
            // hangs below it to widen.
            Matcher::Glob(_) => "",
            Matcher::Segment(_) => path,
        }
    }

    /// The root this matcher occupies IN `path` โ€” the naming test's left-hand side.
    ///
    /// Computed from the match POSITION, not from the matcher's text, because a `Segment` has no
    /// fixed root: `.aws` roots at `~/.aws` in one path and at `~/projects/app/.aws` in another,
    /// and comparing against the bare text `".aws"` would rank every grant as being below it.
    fn root_in(&self, path: &str, fold: bool) -> Option<String> {
        self.specificity(path, fold)?;
        Some(match self {
            Matcher::Exact(s) => s.clone(),
            Matcher::Prefix(s) => s.strip_suffix('/').unwrap_or(s).to_string(),
            Matcher::StringPrefix(s) => s.clone(),
            Matcher::Glob(_) => path.to_string(),
            // SLICED from `path`, never rebuilt by joining components: an absolute path's leading
            // `/` is an empty first component, so re-joining silently produced `root/.ssh` for
            // `/root/.ssh/id_rsa` and the grant that named it then failed to match its own node.
            Matcher::Segment(seg) => {
                let eq = |a: &str, b: &str| if fold { a.eq_ignore_ascii_case(b) } else { a == b };
                let mut offset = 0usize;
                let mut end = None;
                for comp in path.split('/') {
                    if eq(comp, seg) {
                        end = Some(offset + comp.len());
                        break;
                    }
                    offset += comp.len() + 1;
                }
                path[..end?].to_string()
            }
        })
    }
}

/// Whether `inner` sits at or below `outer` as a path, by whole components.
///
/// `~/.ssh` is at-or-below `~/.ssh` and `~/.ssh/known_hosts` is below it, but `~/.sshfoo` is not โ€”
/// hence the component boundary rather than a bare `starts_with`.
fn at_or_below(inner: &str, outer: &str) -> bool {
    let inner = inner.trim_end_matches('/');
    let outer = outer.trim_end_matches('/');
    // An empty `outer` would make every absolute path read as "below" it. No node roots at `/`
    // today, so this is a fail-closed backstop rather than a live case: nothing NAMES a node that
    // claims the whole filesystem.
    if outer.is_empty() {
        return false;
    }
    inner == outer || inner.strip_prefix(outer).is_some_and(|rest| rest.starts_with('/'))
}

/// Whether `remainder` (a path below a grant root) contains a hidden component โ€” a dotfile/
/// dotdir like `.ssh`, `.env`, `.git-credentials`. Credentials and config live in these, so a
/// broad grant must not sweep them up; grant such a directory explicitly to reach inside it.
fn has_hidden_component(remainder: &str) -> bool {
    remainder.split('/').any(|seg| seg.len() > 1 && seg.starts_with('.'))
}

/// ASCII-case-insensitive `starts_with`, zero-alloc (for case-folded shield matching).
fn ci_starts_with(haystack: &str, prefix: &str) -> bool {
    haystack.len() >= prefix.len()
        && haystack.as_bytes()[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
}

/// Whether a role is a PROTECTION (a credential/secret shield, the pinned config, or a
/// write-freeze) rather than an admit โ€” it makes some face stricter than an ordinary worktree.
/// Only protection nodes are matched case-insensitively on a case-insensitive filesystem: folding
/// an ADMIT (`/tmp`, worktree) could admit a case-variant that is a DIFFERENT path on a
/// case-sensitive volume (fail-open), whereas folding a protection only ever denies more.
fn role_is_protective(role: &Role) -> bool {
    role.reads_secret
        || role.frozen != Frozen::Nothing
        || role.write_locus > LocalLocus::Worktree
        || role.read_locus > LocalLocus::WorktreeTrusted
}

struct Node {
    matcher: Matcher,
    role: Role,
    os: Option<Vec<String>>,
    /// Match this node's path case-insensitively on a case-insensitive filesystem โ€” set for
    /// protection nodes only (see `role_is_protective`).
    fold: bool,
}

impl Node {
    fn applies_here(&self) -> bool {
        // A SHIELD applies on every platform, whatever its `os` list says. Over-denying a path that
        // does not exist on this OS costs nothing โ€” nobody reads `/etc/shadow` on macOS โ€” whereas
        // under-denying it is a credential leak.
        //
        // Harmless while an unmatched path fell through to `unknown`/`machine` and denied anyway:
        // the os-gated shields were belt on top of braces. They stop being harmless the moment local
        // reads open up, because that removes the braces. Measured on the experimental read-opening:
        // `perl -pe 's/a/b/' /etc/shadow` โ€” a MUST-DENY composition invariant โ€” auto-approved on
        // macOS, because the node carrying `/etc/shadow` is `os = ["linux"]`.
        //
        // An `os` list is a statement about where a path is MEANINGFUL. That is a reasonable thing
        // to record for an admit (a case-variant of `/tmp` really is a different directory on a
        // case-sensitive volume); for a shield it is a reason to deny more, never less.
        if self.role.reads_secret {
            return true;
        }
        match &self.os {
            None => true,
            Some(list) => list.iter().any(|o| o == current_os()),
        }
    }
}

#[cfg(test)]
thread_local! {
    static OS_OVERRIDE: std::cell::Cell<Option<&'static str>> = const { std::cell::Cell::new(None) };
}

/// Run `f` with the platform forced (tests only): lets the scenario suite exercise BOTH the
/// linux and macOS region sets on any host, instead of `cfg`-gating half of them away.
#[cfg(test)]
pub(crate) fn with_os<T>(os: &'static str, f: impl FnOnce() -> T) -> T {
    struct Reset(Option<&'static str>);
    impl Drop for Reset {
        fn drop(&mut self) {
            OS_OVERRIDE.with(|c| c.set(self.0));
        }
    }
    let _reset = Reset(OS_OVERRIDE.with(|c| c.replace(Some(os))));
    f()
}

pub(super) fn current_os() -> &'static str {
    #[cfg(test)]
    if let Some(o) = OS_OVERRIDE.with(std::cell::Cell::get) {
        return o;
    }
    std::env::consts::OS
}

struct Regions {
    nodes: Vec<Node>,
    worktree: Role,
    unknown: Role,
}

fn parse_locus(s: &str) -> LocalLocus {
    LocalLocus::from_term(s).unwrap_or_else(|| panic!("regions: unknown locus rung `{s}`"))
}

static REGIONS: LazyLock<Regions> = LazyLock::new(|| {
    let src = include_str!("../../../regions/default.toml");
    let file: RegionsFile = toml::from_str(src).expect("regions/default.toml is invalid TOML");

    let role_of = |name: &str| -> Role {
        let def = file
            .role
            .get(name)
            .unwrap_or_else(|| panic!("regions: role `{name}` is not defined"));
        let write_locus = parse_locus(&def.write_locus);
        Role {
            read_locus: parse_locus(&def.read_locus),
            write_locus,
            // Absent โ†’ the same rung as the write face. The two only diverge under a grant, or
            // where a role states the rebind face explicitly.
            rebind_locus: def.rebind_locus.as_deref().map(parse_locus).unwrap_or(write_locus),
            reads_secret: def.reads_secret,
            frozen: match def.frozen.as_deref() {
                None => Frozen::Nothing,
                Some("rebind") => Frozen::Rebind,
                Some("write") => Frozen::Write,
                Some(other) => panic!("regions: role `{name}` has unknown frozen face `{other}` (known: rebind, write)"),
            },
        }
    };

    // ONE node per declaration. An earlier pass generated an absolute twin for every `~/` node so
    // that `/Users/you/.config` would match `~/.config`. That was the right observation at the
    // wrong layer: `canonicalize` already folds an absolute home path to `~`, and `classify_region`
    // now calls it, so the twins did a job the choke point does โ€” while doubling the table and
    // pinning it to whatever `$HOME` happened to be when the `LazyLock` first ran.
    let nodes = file
        .region
        .iter()
        .map(|r| {
            let role = role_of(&r.role);
            Node {
                matcher: Matcher::from_path(&r.path),
                role,
                os: r.os.clone(),
                fold: role_is_protective(&role),
            }
        })
        .collect();

    Regions {
        nodes,
        worktree: role_of("worktree"),
        unknown: role_of("unknown"),
    }
});

// โ”€โ”€ User trust grants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// A user WIDENS the default classification for directories they own by listing them in
// `~/.config/safe-chains.toml`. A grant admits reads and/or writes under a subtree โ€” the
// read/write asymmetry is the point (`read = true, write = false` = a readable-but-not-written
// install dir). Grants only ever widen, are user-level only (never a repo file โ€” an agent
// could drop one to escalate), and NEVER override a secret carve-out (`~/.ssh/id_rsa` stays
// denied even under a `~/` grant).

/// Where a grant came from. Only a grant the user wrote FOR safe-chains may name a credential
/// store: a `Read(~/.ssh/**)` rule in `~/.claude/settings.json` was written to answer Claude's
/// permission prompt, and does not say the user wants every command touching `~/.ssh` auto-approved
/// here. Borrowing those rules is a convenience, and it stops short of the expensive case.
#[derive(Clone, Copy, PartialEq)]
enum GrantSource {
    UserConfig,
    Derived,
}

struct Grant {
    matcher: Matcher,
    read: bool,
    write: bool,
    source: GrantSource,
}

// Grants are read from the user config in the real binary; tests inject them via `with_grants`.
#[cfg(not(test))]
#[derive(Deserialize)]
struct GrantEntry {
    path: String,
    #[serde(default)]
    read: bool,
    #[serde(default)]
    write: bool,
}

#[cfg(not(test))]
#[derive(Deserialize)]
struct GrantFile {
    #[serde(default)]
    grant: Vec<GrantEntry>,
}

#[cfg(not(test))]
fn load_user_grants() -> Vec<Grant> {
    if std::env::var_os("SAFE_CHAINS_NO_LOCAL").is_some() {
        return Vec::new();
    }
    let Some(home) = std::env::var_os("HOME").map(std::path::PathBuf::from) else {
        return Vec::new();
    };
    let mut grants = Vec::new();
    // ~/.config/safe-chains.toml โ€” safe-chains' own grant list (read and/or write). XDG is
    // deliberately not honored so a redirected env var can't point the trust root at an
    // agent-writable dir (see custom.rs).
    if let Ok(src) = std::fs::read_to_string(home.join(".config/safe-chains.toml")) {
        grants.extend(
            toml::from_str::<GrantFile>(&src)
                .map(|f| f.grant)
                .unwrap_or_default()
                .into_iter()
                .flat_map(|g| {
                    grant_matchers(&g.path)
                        .into_iter()
                        .map(move |m| Grant { matcher: m, read: g.read, write: g.write, source: GrantSource::UserConfig })
                }),
        );
    }
    // ~/.claude/settings.json Read(...) rules โ€” the harness's own read approvals, honored
    // read-only (an Edit()/Write() rule never becomes a write grant). The command-grant
    // analogue lives in `allowlist.rs`.
    // Same scoping as the command bridge: these are Claude's own `Read(...)` approvals, so they
    // count only when Claude is the harness. Unconditionally, a Claude file was widening path
    // grants under Codex and every other target (`crate::trust_claude_config`).
    if crate::claude_config_trusted() {
        grants.extend(claude_settings_read_grants(&home));
    }
    grants
}

/// A Claude Code `Read(<pattern>)` permission rule translated into a grant-path prefix โ€” or
/// `None` when the pattern can't be a clean prefix. Only ABSOLUTE (`//โ€ฆ`) and HOME (`~/โ€ฆ`)
/// patterns become grants: a relative / gitignore-style rule describes a workspace-local read
/// that is already auto-approved, so there is nothing to widen. The result is trimmed to a
/// glob-free prefix; a mid-path glob (`//Users/*/x`) or a bare filesystem/home root is refused
/// (fail closed โ€” a "read anything" harness rule does not turn into a filesystem free pass; the
/// user can still grant that explicitly in `~/.config/safe-chains.toml`).
fn translate_read_pattern(inner: &str) -> Option<String> {
    let inner = inner.trim();
    let base = if let Some(rest) = inner.strip_prefix("//") {
        format!("/{rest}")
    } else if inner == "~" || inner.starts_with("~/") {
        inner.to_string()
    } else {
        return None;
    };
    // Strip a trailing directory glob; remember we did, so the grant becomes a subtree Prefix
    // (trailing slash) rather than an Exact single-path match โ€” see `Matcher::from_path`.
    let mut prefix = base.as_str();
    let mut had_glob = false;
    while let Some(p) = prefix.strip_suffix("/**").or_else(|| prefix.strip_suffix("/*")) {
        prefix = p;
        had_glob = true;
    }
    let prefix = prefix.strip_suffix('/').unwrap_or(prefix);
    if prefix.contains(['*', '?']) || prefix.is_empty() || prefix == "/" || prefix == "~" {
        return None;
    }
    Some(if had_glob { format!("{prefix}/") } else { prefix.to_string() })
}

/// Grant-path prefixes derived from `Read(...)` allow-rules in a Claude Code `settings.json`
/// body. Only `permissions.allow` is consulted โ€” the same trusted field the command allowlist
/// reads (see `allowlist.rs`).
fn claude_read_grant_paths(settings_json: &str) -> Vec<String> {
    let Ok(value) = serde_json::from_str::<serde_json::Value>(settings_json) else {
        return Vec::new();
    };
    let Some(arr) = value
        .get("permissions")
        .and_then(|v| v.get("allow"))
        .and_then(|v| v.as_array())
    else {
        return Vec::new();
    };
    arr.iter()
        .filter_map(|e| e.as_str())
        .filter_map(|entry| entry.strip_prefix("Read(").and_then(|s| s.strip_suffix(')')))
        .filter_map(translate_read_pattern)
        .collect()
}

/// Read-only grants sourced from `~/.claude/settings.json`. Only the user-global home settings
/// are trusted; a project's `.claude/settings.json` lives in the tree the agent edits and is
/// never read (mirrors `allowlist.rs`).
fn claude_settings_read_grants(home: &std::path::Path) -> Vec<Grant> {
    let Ok(src) = std::fs::read_to_string(home.join(".claude/settings.json")) else {
        return Vec::new();
    };
    claude_read_grant_paths(&src)
        .into_iter()
        .flat_map(|p| {
            grant_matchers(&p)
                .into_iter()
                .map(|m| Grant { matcher: m, read: true, write: false, source: GrantSource::Derived })
        })
        .collect()
}

/// The matcher(s) for a grant path: the path as written, PLUS the other spelling of a home path
/// so a `~/` grant and a `/Users/you/` grant both cover a home file however the agent spells it.
fn grant_matchers(path: &str) -> Vec<Matcher> {
    let home = || std::env::var_os("HOME").and_then(|h| h.into_string().ok());
    let mut out = vec![Matcher::from_path(&as_subtree(path))];
    if let Some(rest) = path.strip_prefix('~') {
        if let Some(h) = home() {
            out.push(Matcher::from_path(&as_subtree(&format!("{h}{rest}"))));
        }
    } else if let Some(h) = home()
        && let Some(rest) = path.strip_prefix(h.as_str())
    {
        out.push(Matcher::from_path(&as_subtree(&format!("~{rest}"))));
    }
    out
}

/// A grant path in subtree form: `~/.ssh` covers `~/.ssh/id_rsa`, not just the directory entry.
///
/// Without this a grant written the natural way is nearly inert. `Matcher::from_path` reads a path
/// with no trailing slash as `Exact`, which matches the directory itself and nothing inside it, so
/// `path = "~/projects"` would grant only `~/projects` while `path = "~/projects/"` granted the
/// tree. Nobody means the first. A `Prefix` still matches the bare directory too, so the stricter
/// reading is not lost, and a `*` path keeps its `StringPrefix` form.
fn as_subtree(path: &str) -> String {
    if path.ends_with('*') || path.ends_with('/') { path.to_string() } else { format!("{path}/") }
}

#[cfg(not(test))]
static USER_GRANTS: LazyLock<Vec<Grant>> = LazyLock::new(load_user_grants);

#[cfg(test)]
thread_local! {
    static TEST_GRANTS: std::cell::RefCell<Vec<Grant>> = const { std::cell::RefCell::new(Vec::new()) };
}

/// Run `f` with the given grants active (tests only): `(path, read, write)`. These carry
/// user-config semantics, so they may name a credential store; `with_derived_grants` is the
/// borrowed-from-another-tool flavor that may not.
#[cfg(test)]
pub(crate) fn with_grants<T>(grants: &[(&str, bool, bool)], f: impl FnOnce() -> T) -> T {
    with_grants_of_kind(grants, GrantSource::UserConfig, f)
}

/// Run `f` with grants that came from another tool's config (tests only) โ€” read-only in practice,
/// and never able to name a credential store.
#[cfg(test)]
pub(crate) fn with_derived_grants<T>(grants: &[(&str, bool, bool)], f: impl FnOnce() -> T) -> T {
    with_grants_of_kind(grants, GrantSource::Derived, f)
}

#[cfg(test)]
fn with_grants_of_kind<T>(grants: &[(&str, bool, bool)], source: GrantSource, f: impl FnOnce() -> T) -> T {
    let parsed = grants
        .iter()
        .flat_map(|&(p, read, write)| {
            grant_matchers(p).into_iter().map(move |m| Grant { matcher: m, read, write, source })
        })
        .collect();
    TEST_GRANTS.with(|g| *g.borrow_mut() = parsed);
    let out = f();
    TEST_GRANTS.with(|g| g.borrow_mut().clear());
    out
}

/// The most-specific grant matching `path`, as `(read, write)`.
///
/// `secret_root`, when set, is the root of the credential-store node covering `path`, and switches
/// on the NAMING test: a grant reaches a secret only if it names it, meaning the grant's own root
/// sits at or below that node's root. `~/.ssh` and `~/.ssh/known_hosts` name `~/.ssh`; `~/` does
/// not. Someone who grants a path inside a store has decided about the store as surely as someone
/// who grants the store itself.
///
/// The hidden-component rule below cannot stand in for this. It only covers stores that are
/// dot-prefixed, and most are not โ€” `/etc/shadow`, `/root`, `~/Library/Keychains`,
/// `~/Library/Messages` and every browser profile under `Application Support` all have dot-free
/// remainders, so without the naming test a `~/Library/` grant would silently unlock Keychains,
/// Safari history and Messages.
fn best_grant(path: &str, secret_root: Option<&str>) -> Option<(bool, bool)> {
    let pick = |grants: &[Grant]| {
        grants
            .iter()
            .filter_map(|g| {
                let spec = g.matcher.specificity(path, false)?;
                if let Some(root) = secret_root {
                    if g.source != GrantSource::UserConfig {
                        return None;
                    }
                    // Grants never fold case, so the root is computed unfolded here too; a grant
                    // covers the spelling it names (see `role_is_protective`).
                    let grant_root = g.matcher.root_in(path, false)?;
                    if !at_or_below(&grant_root, root) {
                        return None;
                    }
                }
                // A grant never widens a hidden file/dir it happened to sweep up (`~/` grant vs
                // `~/.git-credentials`); grant the dotdir explicitly to reach inside it.
                (!has_hidden_component(g.matcher.remainder(path))).then_some((spec, g.read, g.write))
            })
            .max_by_key(|&(s, ..)| s)
            .map(|(_, r, w)| (r, w))
    };
    #[cfg(test)]
    {
        TEST_GRANTS.with(|g| pick(&g.borrow()))
    }
    #[cfg(not(test))]
    {
        pick(&USER_GRANTS)
    }
}

/// Widen `base` by a matching user grant. Each face is admitted only if the grant grants it โ€”
/// `read`/`write` are independent.
///
/// A grant covers what it NAMES. That is the whole rule, and a credential store is not an
/// exception to it: a grant naming `~/.ssh` reaches `~/.ssh`, a grant on `~/` does not (the naming
/// test lives in `best_grant`). Forcing a user to acknowledge that they meant it is not this
/// program's job; declining to let a grant reach somewhere it never mentioned is.
///
/// Faces that stay frozen regardless of naming, because granting them forfeits the ability to
/// enforce anything afterwards:
///   - `frozen = "write"` โ€” safe-chains' own config and the harness settings file it reads
///     permissions from. An agent that can write one can decide what gets approved next.
///   - `frozen = "rebind"` โ€” the directories those files live in. Writing a file INTO `~/.config`
///     is ordinary; replacing `~/.config` itself points the trust root somewhere else.
///   - `system-integrity` โ€” `/etc/passwd`, `/etc/sudoers`, `/etc/pam.d`, the loader and boot. These
///     decide who may log in and what they may do, so a write there is compromise-complete.
///
/// All stay READABLE by a naming grant; it is only the writing face that cannot be handed over.
fn apply_grant(path: &str, base: Role) -> Role {
    // Fail closed: a secret role whose node we cannot locate gets no grant at all, rather than
    // falling through to the un-named case.
    let secret_root = if base.reads_secret {
        match secret_node_root(path) {
            Some(root) => Some(root),
            None => return base,
        }
    } else {
        None
    };
    let Some((read, write)) = best_grant(path, secret_root.as_deref()) else {
        return base;
    };
    Role {
        read_locus: if read { base.read_locus.min(LocalLocus::WorktreeTrusted) } else { base.read_locus },
        write_locus: if write && base.write_grantable() {
            base.write_locus.min(LocalLocus::Worktree)
        } else {
            base.write_locus
        },
        rebind_locus: if write && base.rebind_grantable() {
            base.rebind_locus.min(LocalLocus::Worktree)
        } else {
            base.rebind_locus
        },
        // A READ grant that NAMED the store clears the shield for it. Without this the grant moved
        // the locus and nothing else, so `[[grant]] path = "~/.ssh", read = true` still refused โ€”
        // `secret ยท reads` is admitted by no level below yolo, so the flag alone decided it. The
        // user's first lever did nothing and nothing said why, which is the symptom this fixes.
        //
        // Reaching here already proves the grant named the store rather than sweeping it up:
        // `best_grant` returns None unless the grant root is `at_or_below` the shielded node AND
        // the remainder carries no hidden component, so a `~/` grant cannot clear `~/.ssh` โ€” it is
        // ABOVE the node, not at or inside it. That is the rule the hidden-file carve-out already
        // states in its own comment: "grant such a directory explicitly to reach inside it."
        //
        // Only the READ face. A grant that says `read = true` says nothing about writing, and the
        // write faces stay behind `write_grantable`/`rebind_grantable`, which is where `pinned`
        // keeps its blanket refusal โ€” safe-chains' own config stays un-grantable however
        // specifically it is named, because that risk is to the mechanism rather than to the
        // user's data.
        //
        // The user config is the trust root: user-owned and unwritable by an agent. A grant typed
        // there IS the statement of intent, which is why an extra `acknowledge` field was rejected
        // as ceremony rather than safety.
        reads_secret: base.reads_secret && !read,
        frozen: base.frozen,
    }
}

/// The role for `path`. Most-specific applicable node wins; ties break toward the more
/// restrictive role (higher write locus, then read locus) โ€” a safety backstop. No match โ†’
/// fail-closed default: an absolute or home path is `unknown` (deny), a relative one is
/// `worktree`. Then a user trust grant may widen the result. `path` is expected already
/// resolved and past the `$`/`..` guard.
pub(crate) fn classify_region(path: &str) -> Role {
    // Canonicalized HERE, at the choke point, rather than by each caller. `classify_pinned` did it
    // and the other two callers did not, so `names_credential_store` and `frozen_write_kind` judged
    // raw spellings: `/Users/you/.config` and `~/.config/` are the same directory, and only the
    // enforcement path knew it. Enforcement was never wrong, but the NUDGE was, which is how a
    // denial for one reason came out explained as another.
    let path = &super::locus::canonicalize(path);
    if let Some(role) = scratchpad_role(path) {
        return role;
    }
    apply_grant(path, base_region(path))
}

/// This session's SCRATCHPAD โ€” the harness's own per-session working directory โ€” earns
/// `sandbox-scope`: a trusted working area that is not the worktree.
///
/// Why a distinct rung rather than just `temp`: the scratchpad is where an agent stages its OWN
/// work (a generated script, an extracted archive, intermediate data). Classified `temp` it is read-
/// and write-able but **not executable**, because `temp` sits BELOW the execute clause's
/// `>= sandbox-scope` floor โ€” the floor that (correctly) treats `/tmp/x.sh` as downloaded, foreign
/// code. That floor is right for anonymous `/tmp` and wrong for the agent's own workspace, and
/// `sandbox-scope` is precisely the rung the level model reserved for "trusted, not the worktree"
/// (levels/default.toml, the executor-origin band). So a recognized scratchpad becomes runnable
/// while every other `/tmp` path stays foreign.
///
/// Recognition is anchored on the unforgeable session id, never on a guessable layout โ€” see
/// `pathctx::in_session_scratchpad` for why that is both safe and durable. This runs BEFORE the
/// region table so the scratchpad is not first captured by the generic `/tmp` node; it deliberately
/// does NOT bypass anything else, because a non-matching path falls straight through to the normal
/// classification.
fn scratchpad_role(path: &str) -> Option<Role> {
    crate::pathctx::in_session_scratchpad(path).then_some(Role {
        read_locus: LocalLocus::SandboxScope,
        write_locus: LocalLocus::SandboxScope,
        rebind_locus: LocalLocus::SandboxScope,
        reads_secret: false,
        frozen: Frozen::Nothing,
    })
}

/// The credential-store node covering `path`, if any. Shared by `base_region` (which wants the
/// role) and `apply_grant` (which wants the root the naming test compares against) so the two can
/// never disagree about which node is in play.
fn secret_node(path: &str) -> Option<&'static Node> {
    shield_spellings(path).into_iter().find_map(|p| {
        REGIONS
            .nodes
            .iter()
            .filter(|n| n.applies_here() && n.role.reads_secret)
            .find(|n| n.matcher.specificity(&p, n.fold && current_os() == "macos").is_some())
    })
}

/// Every spelling a SHIELD must be asked about: the path itself, plus its macOS firmlink form
/// when the `/private` prefix only matches under case folding.
///
/// `canonicalize` already folds `/private/etc/x` โ†’ `/etc/x`, but case-sensitively, because that
/// fold also feeds admits and folding an admit would hand `/PRIVATE/tmp` the scratch rung. So the
/// case variant of a firmlink never reached the shield: `/etc/SHADOW` was refused (nodes fold) and
/// `/PRIVATE/etc/shadow` was not, though on the default macOS volume they are one file.
///
/// Extra spellings are offered to the shield ONLY. Everything else classifies the path as written,
/// which is what keeps a case variant from reaching a permissive node.
fn shield_spellings(path: &str) -> Vec<String> {
    let mut out = vec![path.to_string()];
    if let Some(folded) = super::locus::firmlink_fold_case_insensitive(path)
        && folded != path
    {
        out.push(folded);
    }
    out
}

/// The root the naming test compares a grant against: the DEEPEST matching credential-store node.
///
/// Deepest, not first-declared. A path can match more than one secret node (`/root/.ssh/id_rsa`
/// matches both the `/root/` prefix and the `.ssh` segment), and the deeper root is the safer
/// choice because it demands a more specific grant to name it. Taking whichever node the table
/// happened to declare first would let a shallower root govern a store nested inside it, which is
/// the failure the naming test exists to prevent. Today the segment nodes are declared first and
/// happen to give the deeper root; that is an accident of file order, not a property to rely on.
fn secret_node_root(path: &str) -> Option<String> {
    let fold_shields = current_os() == "macos";
    shield_spellings(path)
        .iter()
        .flat_map(|p| {
            REGIONS
                .nodes
                .iter()
                .filter(|n| n.applies_here() && n.role.reads_secret)
                .filter_map(move |n| n.matcher.root_in(p, n.fold && fold_shields))
        })
        .max_by_key(|r| (r.split('/').count(), r.len()))
}

fn base_region(path: &str) -> Role {
    let r = &*REGIONS;
    // macOS's default filesystem (APFS) is case-insensitive, so `~/.AWS` and `.GIT/hooks` name the
    // same files as `~/.aws`/`.git` โ€” a shield must fire on the case-variant too. Admit nodes are
    // never folded (a case-variant of `/tmp` on a case-sensitive volume is a different dir).
    // Best-effort by OS, not by volume: safe-chains never inspects the filesystem (ยง0.2, TOCTOU), so
    // a NON-default case-insensitive Linux mount (ext4 `casefold`, ciopfs, vfat) is not covered, and
    // a case-sensitive macOS volume over-denies a genuinely-distinct `.GIT` (fail-safe). Matching the
    // OS is the honest proxy for the default case.
    let fold_shields = current_os() == "macos";

    // A SECRET-BEARING region wins outright, before specificity is considered at all.
    //
    // Specificity ranks exact โ‰ซ prefix โ‰ซ segment, so ANY subtree admit outranks the shield's
    // segment match however deep the shield sits. Adding read-admits for package content made that
    // concrete: `/usr/share/.ssh/id_rsa` was approved, because `/usr/share/` is a prefix and
    // `.ssh` is only a segment. The shield's whole purpose is to be un-widenable โ€” `apply_grant`
    // already refuses to let a grant reach through it, and an admit node must not either.
    //
    // This is the same failure that retired the previous admit map: a broad prefix quietly
    // swallowing something sensitive underneath it.
    if let Some(node) = secret_node(path) {
        return node.role;
    }

    let mut best: Option<(usize, Role)> = None;
    for node in &r.nodes {
        if !node.applies_here() {
            continue;
        }
        let Some(spec) = node.matcher.specificity(path, node.fold && fold_shields) else {
            continue;
        };
        let take = match best {
            None => true,
            Some((bs, br)) => spec > bs || (spec == bs && more_restrictive(node.role, br)),
        };
        if take {
            best = Some((spec, node.role));
        }
    }
    if let Some((_, role)) = best {
        return role;
    }
    if path.starts_with('/') || path.starts_with('~') {
        // A specific region (credential shield, .git freeze) already won above; only a path matching
        // NOTHING reaches here. If it is a SIBLING of the workspace, it earns `adjacent` (a peer
        // project) rather than the `unknown`/machine deny โ€” the co-located-repo pattern.
        //
        // Order is load-bearing. Home comes AFTER adjacency: expressed instead as a `~/` node in the
        // region table it matched by subtree specificity up in the loop above and SHADOWED the
        // sibling test, so every peer project (which lives under `$HOME` too) stopped being
        // `adjacent` and `touch ../branchdiff/x` began denying. Sibling-ness is structural; home is
        // the fallback for whatever under `$HOME` is neither.
        adjacent_role(path)
            .or_else(|| other_user_home_role(path))
            .or_else(|| home_role(path))
            .unwrap_or(r.unknown)
    } else {
        r.worktree
    }
}

/// ANOTHER user's home (`~root`, `~alice`): shielded, exactly like a credential store.
///
/// Checked before `home_role` and stated as a secret rather than left to the locus, because the
/// locus is what opens up: with local reads admitted to `<= machine`, `~root/.bashrc` would
/// otherwise become an ordinary readable file. `/root/` is already a declared shield and this is the
/// same claim by a different spelling โ€” a home that is not ours is private data whatever its rung.
fn other_user_home_role(path: &str) -> Option<Role> {
    // `~` and `~/โ€ฆ` are OUR home; `~name` / `~name/โ€ฆ` is someone else's.
    let tilde_named = path.strip_prefix('~').is_some_and(|r| !r.is_empty() && !r.starts_with('/'));
    (tilde_named || absolute_other_home(path)).then_some(Role {
        read_locus: LocalLocus::Machine,
        write_locus: LocalLocus::Machine,
        rebind_locus: LocalLocus::Machine,
        reads_secret: true,
        frozen: Frozen::Nothing,
    })
}

/// `/Users/someone/โ€ฆ`, `/home/someone/โ€ฆ` โ€” another user's home written out in full.
///
/// Only the `~name` spelling was recognised, and while every machine-rung path was refused that
/// was enough. Opening local reads made the two spellings disagree: `~someone/notes` stayed
/// private and `/Users/someone/notes` read out. Our OWN home never arrives here as an absolute โ€”
/// `canonicalize` has already folded it to `~/โ€ฆ`.
fn absolute_other_home(path: &str) -> bool {
    let home = std::env::var("HOME").unwrap_or_default();
    let ours = home.rsplit_once('/').map(|(_, name)| name).unwrap_or_default();
    // Both conventional roots, not just this host's: the classifier reads strings, and a Linux
    // path handed to a macOS run must not come back with a different answer.
    ["/Users", "/home"].iter().any(|parent| {
        path.strip_prefix(parent)
            .and_then(|r| r.strip_prefix('/'))
            .and_then(|r| r.split('/').next())
            .is_some_and(|user| !user.is_empty() && (ours.is_empty() || user != ours))
    })
}

/// Anything under `$HOME` that matched no node and is not a sibling: readable, not writable.
///
/// This is the first production construction site `LocalLocus::User` has ever had. The rung was
/// defined (`User => "user", // ~, keychain`) and the level ladder was authored against it, but no
/// path ever resolved to it โ€” every `~` path fell through to `unknown` and so to `machine`, the same
/// rung as `/etc/hosts`. Three authoring assertions passed while describing behaviour production
/// could not reach, and "read all of `~`, write none of it" was inexpressible because both halves
/// were the same term.
/// DOTFILES are included. `~/.zshrc`, `~/.gitconfig` and a tool's config under `~/.config` are
/// ordinary reads, and excluding them was never what protected the credential-bearing ones: an
/// excluded path falls through to `unknown` โ†’ `machine`, and `machine` reads are exactly what the
/// read policy admits. Measured before the shield was extended: `cat ~/.git-credentials` ALLOWED,
/// indistinguishable from `~/.zshrc`.
///
/// What protects those files is the shield NAMING them โ€” `.git-credentials`, `.npmrc`, `.pypirc`,
/// `.pgpass`, `.my.cnf`, `.dockercfg`, the cargo/gem/maven/gradle/composer token files โ€” and the
/// shield wins outright in `base_region`, before specificity is considered at all. That is why the
/// dotfile research had to land first and this exclusion could then go.
///
/// The residual risk is stated rather than hidden: the shield is an enumeration, so a tool that
/// invents a new credential dotfile is readable until someone declares it. That is the accepted
/// shape of the one denylist this project keeps.
fn home_role(path: &str) -> Option<Role> {
    if path != "~" && !path.starts_with("~/") {
        return None;
    }
    Some(Role {
        read_locus: LocalLocus::User,
        write_locus: LocalLocus::Machine,
        rebind_locus: LocalLocus::Machine,
        reads_secret: false,
        frozen: Frozen::Nothing,
    })
}

/// Classify `path` as a direct SIBLING of the workspace โ€” a peer project under the same parent
/// (`../branchdiff/src/x`) โ€” earning the `adjacent` role (reads at reader, create/mutate at
/// developer; DESTROY stays worktree-only via the levels). `None` (โ†’ `unknown`, denied) unless every
/// guard holds:
///  - the workspace root sits at depth >= 2 below `$HOME`, so its parent is never `$HOME` itself
///    (else a workspace at `~/work` would make `~/.ssh` a "sibling"); outside `$HOME`, no adjacency.
///  - the path is strictly UNDER the parent and NOT under the workspace itself.
///  - no HIDDEN (dot) component in the remainder below the parent โ€” mirrors the grant shield
///    (`has_hidden_component`): a peer project's `.env`/`.git`/`.aws` stays denied, never adjacent.
///
/// `path` is already canonicalized to `~`-form; the workspace root is normalized to match.
fn adjacent_role(path: &str) -> Option<Role> {
    matches!(peer_kind(path), PeerKind::Ordinary).then_some(Role {
        read_locus: LocalLocus::Adjacent,
        write_locus: LocalLocus::Adjacent,
        rebind_locus: LocalLocus::Adjacent,
        reads_secret: false,
        frozen: Frozen::Nothing,
    })
}

/// Every region PATH the model declares, straight from `regions/default.toml`. Test-only, and it
/// exists so the abstraction-soundness property draws its witnesses from the region table rather
/// than from a hand-picked list: a newly-protected path becomes a witness the moment it is
/// declared, without anyone remembering to extend a corpus.
#[cfg(test)]
pub(crate) fn declared_region_paths() -> Vec<String> {
    let src = include_str!("../../../regions/default.toml");
    let file: RegionsFile = toml::from_str(src).expect("regions/default.toml is invalid TOML");
    file.region.into_iter().map(|r| r.path).collect()
}

enum PeerKind {
    /// A peer project's file โ€” earns `adjacent`.
    ///
    /// Hidden components used to split off a `Hidden` variant here, shielding a peer's `.env`,
    /// `.github` and so on. Removed after a fortnight of real use: it fired constantly on ordinary
    /// committed content (`.github/workflows`, `.vscode`, `.cargo/config.toml`) while the things it
    /// was reaching for โ€” `.ssh`, `.aws`, `.netrc`, `~/.config/gh` โ€” are named by the credential
    /// shield, which is segment-matched and bites at any depth in any project. Hidden-ness was a
    /// second, structural vote on secrecy layered over a shield that already names what is secret,
    /// and it disagreed with the first vote far more often than it added to it.
    ///
    /// What this did give up: a peer's `.env`, `.npmrc` and `.git/config` are now readable โ€” as the
    /// SAME files already were in the workspace the agent is rooted at.
    Ordinary,
    /// Not a co-located peer at all (fails a structural guard).
    NotPeer,
}

/// The single structural truth behind both `adjacent_role` and `is_hidden_peer`: is `path` a
/// co-located peer of the workspace, and if so is it shielded by a hidden component? Every guard is
/// shared so the two callers can never drift.
fn peer_kind(path: &str) -> PeerKind {
    let Some(home) = std::env::var("HOME").ok().filter(|h| h.starts_with('/')) else {
        return PeerKind::NotPeer;
    };
    let Some(root_raw) = crate::pathctx::root() else {
        return PeerKind::NotPeer;
    };
    let root = if root_raw == home {
        "~".to_string()
    } else if let Some(rest) = root_raw.strip_prefix(&home).filter(|r| r.starts_with('/')) {
        format!("~{rest}")
    } else if root_raw.starts_with('~') {
        root_raw
    } else {
        return PeerKind::NotPeer; // workspace outside $HOME (e.g. /opt/app) โ€” conservative
    };
    let root = root.trim_end_matches('/');
    // depth >= 2 below home: root = "~/a/bโ€ฆ" with >= 2 components after "~".
    let Some(stripped) = root.strip_prefix("~/") else {
        return PeerKind::NotPeer;
    };
    let comps: Vec<&str> = stripped.split('/').filter(|s| !s.is_empty()).collect();
    let Some(last) = comps.last().filter(|_| comps.len() >= 2) else {
        return PeerKind::NotPeer;
    };
    let parent = &root[..root.len() - last.len() - 1]; // strip the trailing "/<last>"
    // strictly under the parent โ€ฆ
    let Some(under_parent) = path.strip_prefix(parent).filter(|r| r.starts_with('/')) else {
        return PeerKind::NotPeer;
    };
    // โ€ฆ but NOT the workspace itself or inside it.
    if path == root || path.strip_prefix(root).is_some_and(|r| r.starts_with('/')) {
        return PeerKind::NotPeer;
    }
    let _ = under_parent;
    PeerKind::Ordinary
}

fn more_restrictive(a: Role, b: Role) -> bool {
    (a.write_locus, a.read_locus) > (b.write_locus, b.read_locus)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_region_file_compiles_and_defaults_exist() {
        // forces the LazyLock; panics here if the TOML is malformed or a role is undefined
        let _ = classify_region("/etc/hosts");
        assert_eq!(classify_region("relative/file.txt").write_locus, LocalLocus::Worktree);
        assert_eq!(classify_region("/some/unmapped/path").write_locus, LocalLocus::Machine);
        assert_eq!(classify_region("/some/unmapped/path").read_locus, LocalLocus::Machine);
    }

    #[test]
    fn system_integrity_substrate_write_worst_cases_above_machine() {
        // Identity/auth files (cross-platform): readable as ordinary machine config, but a WRITE
        // worst-cases to system-integrity (above machine โ†’ above local-admin, yolo-only). The
        // loader/boot regions are `os = ["linux"]`, so they're not asserted here (this test is
        // platform-portable); their routing is the same role.
        for p in ["/etc/passwd", "/etc/group", "/etc/sudoers", "/etc/sudoers.d/pkg", "/etc/pam.d/sshd"] {
            assert_eq!(classify_region(p).write_locus, LocalLocus::SystemIntegrity, "write {p}");
            assert_eq!(classify_region(p).read_locus, LocalLocus::Machine, "read {p}");
        }
        // Ordinary /etc app config is NOT the trust substrate โ€” it stays machine (unknown โ†’ machine).
        assert_eq!(classify_region("/etc/nginx/nginx.conf").write_locus, LocalLocus::Machine, "ordinary /etc stays machine");
        assert_eq!(classify_region("/usr/local/bin/tool").write_locus, LocalLocus::Machine, "/usr/local is admin-managed, stays machine");
    }

    /// The sibling-workspace (`adjacent`) classifier and its guards โ€” the edge cases that make it
    /// safe rather than a home-wide hole.
    #[test]
    fn adjacent_sibling_classification() {
        use crate::pathctx::{enter, PathCtx};
        let ws = |root: &str, path: &str| {
            let _g = enter(PathCtx { cwd: Some(root.to_string()), root: Some(root.to_string()), ..Default::default() });
            classify_region(path)
        };
        const WS: &str = "~/projects/safe-chains";

        // A sibling's ORDINARY files โ†’ adjacent (peer project the agent reaches into).
        assert_eq!(ws(WS, "~/projects/branchdiff/src/main.rs").read_locus, LocalLocus::Adjacent);
        assert_eq!(ws(WS, "~/projects/branchdiff/src/main.rs").write_locus, LocalLocus::Adjacent);
        assert_eq!(ws(WS, "~/projects/notes.txt").read_locus, LocalLocus::Adjacent, "a file peer to the workspace dir");

        // A sibling's HIDDEN files are ordinary peer content now โ€” the dot-shield is gone, and what
        // stops a peer's secrets is the credential shield (segment-matched, any depth). `.env` is
        // one the shield does NOT name, so it reads exactly as the same file in the workspace the
        // agent is rooted at already does.
        assert_eq!(ws(WS, "~/projects/branchdiff/.env").read_locus, LocalLocus::Adjacent, "peer .env reads as peer content");
        // `.npmrc` WAS in that category and is not any more: it was added to the shield on
        // 2026-08-15 because it carries `_authToken`, and a project-local copy carries the same one
        // as the home copy. Segment-matched, so the peer's is shielded too โ€” the deliberate
        // over-denial the shield header describes.
        assert_eq!(ws(WS, "~/projects/branchdiff/.npmrc").read_locus, LocalLocus::Machine, "peer .npmrc is a token file, not peer content");
        assert_eq!(ws(WS, "~/projects/branchdiff/.ssh/id_rsa").read_locus, LocalLocus::Machine, "the shield still bites in a peer");
        // The .git WRITE freeze is a separate guard and is unaffected by dropping the dot-shield.
        assert_eq!(ws(WS, "~/projects/branchdiff/.git/hooks/pre-commit").write_locus, LocalLocus::WorktreeTrusted, "peer .git hook stays frozen");

        // THE danger case: a workspace at `~/work` (depth 1) must NOT make `~/.ssh` / `~/x` siblings.
        assert_ne!(ws("~/work", "~/.ssh/id_rsa").read_locus, LocalLocus::Adjacent, "~/.ssh is never adjacent");
        assert_ne!(ws("~/work", "~/other-notes.txt").read_locus, LocalLocus::Adjacent, "depth-1 workspace has no siblings");
        // โ€ฆnor a workspace at `~` itself (depth 0).
        assert_ne!(ws("~", "~/anything.txt").read_locus, LocalLocus::Adjacent);

        // A COUSIN (different parent) is not adjacent.
        assert_ne!(ws(WS, "~/other/thing.txt").read_locus, LocalLocus::Adjacent, "different parent โ†’ not a sibling");
        // A prefix-collision sibling name is a real sibling (peer dir), not the workspace.
        assert_eq!(ws(WS, "~/projects/safe-chains-fork/x").read_locus, LocalLocus::Adjacent);
        // The workspace's own absolute spelling is not "adjacent" (it's the workspace).
        assert_ne!(ws(WS, "~/projects/safe-chains/x").read_locus, LocalLocus::Adjacent);

        // A workspace OUTSIDE $HOME (e.g. /opt) gets no adjacency โ€” conservative.
        assert_ne!(ws("/opt/app", "/opt/other/x").read_locus, LocalLocus::Adjacent);

        // No workspace context โ†’ no adjacency (fail-closed).
        assert_ne!(classify_region("~/projects/branchdiff/src/main.rs").read_locus, LocalLocus::Adjacent);
    }

    /// A peer project's HIDDEN files are ordinary peer content now, and the credential shield is
    /// what still stops the secrets.
    ///
    /// The dot-shield used to freeze every hidden component under a peer. Two weeks of real use
    /// said it fired overwhelmingly on committed project content โ€” `.github/workflows`, `.vscode`,
    /// `.cargo/config.toml` โ€” while everything it was reaching for is NAMED by the credential
    /// shield, which is segment-matched and bites at any depth in any project. It was a second,
    /// structural vote on secrecy over a shield that already names what is secret.
    #[test]
    fn a_peers_hidden_files_are_adjacent_and_the_shield_still_holds() {
        use crate::pathctx::{enter, PathCtx};
        let at = |root: &str, path: &str| {
            let _g = enter(PathCtx { cwd: Some(root.to_string()), root: Some(root.to_string()), ..Default::default() });
            classify_region(path).read_locus
        };
        const WS: &str = "~/projects/safe-chains";

        // Hidden peer content is adjacent โ€” the same rung its ordinary source has.
        for p in [
            "~/projects/branchdiff/.github/workflows/ci.yml",
            "~/projects/branchdiff/.vscode/settings.json",
            "~/projects/branchdiff/.cargo/config.toml",
            "~/projects/branchdiff/.env",
            "~/projects/branchdiff/sub/.config/app.toml",
        ] {
            assert_eq!(at(WS, p), LocalLocus::Adjacent, "hidden peer content should be adjacent: {p}");
        }
        // Ordinary peer source is unchanged.
        assert_eq!(at(WS, "~/projects/branchdiff/src/main.rs"), LocalLocus::Adjacent);

        // The SHIELD is what still refuses, at any depth, in a peer as anywhere else. This is the
        // half that must never regress: removing the dot rule leaned the whole guarantee onto it.
        for p in [
            "~/projects/branchdiff/.ssh/id_rsa",
            "~/projects/branchdiff/.aws/credentials",
            "~/projects/branchdiff/a/b/c/.netrc",
            "~/projects/branchdiff/deep/.gnupg/secring.gpg",
        ] {
            assert_eq!(at(WS, p), LocalLocus::Machine, "the credential shield must still bite: {p}");
        }
        // A cousin under a different parent is still not a peer at all.
        assert_ne!(at(WS, "~/other/notes.txt"), LocalLocus::Adjacent);
    }

    #[test]
    fn most_specific_wins() {
        // the .ssh SEGMENT shield fires at any depth/spelling and reads_secret
        let ssh = classify_region("~/.ssh/id_rsa");
        assert_eq!(ssh.read_locus, LocalLocus::Machine);
        assert!(ssh.reads_secret);
        assert!(classify_region("myproj/.ssh/id_rsa").reads_secret, "segment bites a relative spelling too");
        // ~/notes matches no node โ†’ the home fallback โ†’ `user`, and NOT writable.
        assert_eq!(classify_region("~/notes.txt").read_locus, LocalLocus::User);
        assert_eq!(classify_region("~/notes.txt").write_locus, LocalLocus::Machine);
        // ...while another user's home is shielded rather than merely far away.
        assert!(classify_region("~root/.bashrc").reads_secret, "another user's home is private");
    }

    #[test]
    fn in_project_trusted_files_read_but_do_not_write() {
        let git = classify_region(".git/config");
        assert_eq!(git.read_locus, LocalLocus::WorktreeTrusted, "read is admitted at read-local");
        assert_eq!(git.write_locus, LocalLocus::WorktreeTrusted, "above the worktree write ceiling โ†’ frozen");
    }

    #[test]
    fn user_grant_widens_read_and_write() {
        with_grants(&[("~/projects/", true, true)], || {
            let r = classify_region("~/projects/other/src/main.rs");
            assert_eq!(r.write_locus, LocalLocus::Worktree, "write admitted");
            assert!(r.read_locus <= LocalLocus::WorktreeTrusted, "read admitted");
        });
        // grant gone โ†’ unknown/deny (home is not admitted)
        assert_eq!(classify_region("~/projects/other/src/main.rs").write_locus, LocalLocus::Machine);
    }

    #[test]
    fn read_only_grant_admits_read_but_not_write() {
        with_grants(&[("~/.local/share/mise/", true, false)], || {
            let r = classify_region("~/.local/share/mise/installs/python/bin/python");
            assert!(r.read_locus <= LocalLocus::WorktreeTrusted, "read admitted");
            assert!(r.write_locus > LocalLocus::Worktree, "write NOT admitted");
        });
    }

    #[test]
    fn translate_read_pattern_only_honors_absolute_and_home_prefixes() {
        // a directory glob (`/**`, `/*`) becomes a subtree Prefix (trailing slash)
        assert_eq!(translate_read_pattern("//Users/me/x/**"), Some("/Users/me/x/".into()));
        assert_eq!(translate_read_pattern("~/.gem/**"), Some("~/.gem/".into()));
        assert_eq!(translate_read_pattern("//Users/me/x/*"), Some("/Users/me/x/".into()));
        // a glob-free path stays an exact match (a single file or dir entry)
        assert_eq!(translate_read_pattern("~/.gem"), Some("~/.gem".into()));
        assert_eq!(translate_read_pattern("//etc/hosts"), Some("/etc/hosts".into()));
        // relative / gitignore-style / settings-dir-relative โ†’ workspace-local, not a grant
        assert_eq!(translate_read_pattern("src/**"), None);
        assert_eq!(translate_read_pattern("/logs/**"), None);
        // a mid-path glob can't be represented as a prefix
        assert_eq!(translate_read_pattern("//Users/*/mise/**"), None);
        assert_eq!(translate_read_pattern("~/**/*.pem"), None);
        // a bare filesystem / home root is too broad to honor
        assert_eq!(translate_read_pattern("//**"), None);
        assert_eq!(translate_read_pattern("~/**"), None);
        assert_eq!(translate_read_pattern("~/"), None);
        assert_eq!(translate_read_pattern(""), None);
    }

    #[test]
    fn claude_read_grant_paths_extracts_only_read_allow_rules() {
        let paths = claude_read_grant_paths(
            r#"{"permissions":{"allow":[
                "Bash(ls)","Edit(~/x/**)","Write(~/y/**)","Read(~/z/**)","WebFetch"
            ]}}"#,
        );
        assert_eq!(paths, vec!["~/z/".to_string()]);
        // malformed / missing structure โ†’ empty, never a panic
        assert!(claude_read_grant_paths("not json").is_empty());
        assert!(claude_read_grant_paths("{}").is_empty());
        assert!(claude_read_grant_paths(r#"{"permissions":{}}"#).is_empty());
        // deny/ask rules are not allow-grants
        assert!(claude_read_grant_paths(r#"{"permissions":{"deny":["Read(~/z/**)"]}}"#).is_empty());
    }

    #[test]
    fn claude_read_rule_admits_read_but_never_write() {
        let paths = claude_read_grant_paths(
            r#"{"permissions":{"allow":["Read(~/.local/share/mise/**)","Edit(~/.local/share/mise/**)"]}}"#,
        );
        assert_eq!(paths, vec!["~/.local/share/mise/".to_string()]);
        let grants: Vec<(&str, bool, bool)> = paths.iter().map(|p| (p.as_str(), true, false)).collect();
        with_grants(&grants, || {
            let r = classify_region("~/.local/share/mise/installs/python/bin/python");
            assert!(r.read_locus <= LocalLocus::WorktreeTrusted, "read admitted");
            assert!(r.write_locus > LocalLocus::Worktree, "the Edit() rule is ignored โ€” write stays denied");
        });
    }

    #[test]
    fn a_claude_read_grant_still_respects_the_dot_rule_and_shields() {
        let paths = claude_read_grant_paths(r#"{"permissions":{"allow":["Read(~/work/**)"]}}"#);
        assert_eq!(paths, vec!["~/work/".to_string()]);
        let grants: Vec<(&str, bool, bool)> = paths.iter().map(|p| (p.as_str(), true, false)).collect();
        with_grants(&grants, || {
            assert!(classify_region("~/work/notes.txt").read_locus <= LocalLocus::WorktreeTrusted, "granted read admitted");
            // a hidden credential swept under the grant is still not widened
            assert_eq!(classify_region("~/work/.ssh/id_rsa").read_locus, LocalLocus::Machine, "hidden cred not widened");
        });
    }

    #[test]
    fn claude_settings_read_grants_reads_home_settings_only() {
        let home = tempfile::tempdir().unwrap();
        let claude = home.path().join(".claude");
        std::fs::create_dir_all(&claude).unwrap();
        std::fs::write(
            claude.join("settings.json"),
            r#"{"permissions":{"allow":["Read(~/.gem/**)","Edit(~/.gem/**)"]}}"#,
        )
        .unwrap();
        let grants = claude_settings_read_grants(home.path());
        assert!(!grants.is_empty());
        assert!(grants.iter().all(|g| g.read && !g.write), "Read() rules are read-only");
        // no settings file โ†’ empty, no panic
        let empty = tempfile::tempdir().unwrap();
        assert!(claude_settings_read_grants(empty.path()).is_empty());
    }


    /// A sample path inside `node`, the grant that NAMES it, and the grant on its PARENT.
    ///
    /// `StringPrefix` is skipped (returns `None`): a `/dev/sd*` style matcher has no component
    /// boundary, so "the parent" is not well defined. No credential store uses one today, and the
    /// caller asserts the set it covered is non-empty so this cannot silently empty the guard.
    #[cfg(test)]
    fn naming_probe(matcher: &Matcher) -> Option<(String, String, String)> {
        let parent_of = |s: &str| {
            let t = s.trim_end_matches('/');
            match t.rfind('/') {
                Some(0) => "/".to_string(),
                Some(i) => format!("{}/", &t[..i]),
                None => return None,
            }
            .into()
        };
        match matcher {
            Matcher::Exact(s) => Some((s.clone(), s.clone(), parent_of(s)?)),
            Matcher::Prefix(s) => {
                Some((format!("{s}probe"), s.clone(), parent_of(s)?))
            }
            // BOTH spellings. Probing a segment only as `~/.ssh/probe` left the absolute form
            // unexercised, and that is precisely where the root was being computed wrongly.
            Matcher::Segment(seg) => Some((format!("~/{seg}/probe"), format!("~/{seg}"), "~/".to_string())),
            Matcher::StringPrefix(_) => None,
            // Fill each wildcard with a concrete component, so a Glob node is probed by the
            // grant/shield guards exactly like a spelled-out one.
            Matcher::Glob(pat) => {
                let concrete = pat
                    .iter()
                    .map(|c| if c == "*" { "probe" } else { c.as_str() })
                    .collect::<Vec<_>>()
                    .join("/");
                Some((concrete.clone(), concrete.clone(), parent_of(&concrete)?))
            }
        }
    }

    /// The absolute-form probe for a `Segment` node, which `naming_probe` gives in `~/` form.
    #[cfg(test)]
    fn absolute_segment_probe(matcher: &Matcher) -> Option<(String, String, String)> {
        let Matcher::Segment(seg) = matcher else { return None };
        Some((format!("/opt/app/{seg}/probe"), format!("/opt/app/{seg}"), "/opt/app/".to_string()))
    }

    /// Enumerated over the REAL region table, so a credential store added later is covered without
    /// anyone remembering to extend this.
    ///
    /// The rule: a grant covers what it names. Naming a store reaches it; granting the parent does
    /// not. The second half is the one with teeth, because most stores are NOT dot-prefixed
    /// (`/etc/shadow`, `/root`, `~/Library/Keychains`, `~/Library/Messages`, the browser profiles
    /// under `Application Support`) so the hidden-component rule never fires for them, and without
    /// the naming test a `~/Library/` grant would silently unlock Keychains and Messages.
    /// A grant written without a trailing slash covers the subtree, because that is what anyone
    /// writing it means. Before this, `path = "~/projects"` matched only the directory entry and
    /// nothing inside it, so a grant written the natural way was very nearly inert.
    /// The naming test must compare against the DEEPEST matching secret node, not the first one
    /// declared. `/root/.ssh/id_rsa` matches two: the `/root/` prefix (root `/root`) and the `.ssh`
    /// segment (root `/root/.ssh`). If the shallow one governed, a grant on `/root` would name the
    /// `.ssh` nested inside it. Today the segment nodes are declared first and this holds by
    /// accident of file order, which is exactly why it is pinned here.
    #[test]
    fn the_naming_test_uses_the_deepest_matching_secret_node() {
        with_os("linux", || {
            assert_eq!(secret_node_root("/root/.ssh/id_rsa").as_deref(), Some("/root/.ssh"));
            assert_eq!(secret_node_root("/root/notes.txt").as_deref(), Some("/root"));
            with_grants(&[("/root", true, true)], || {
                assert_eq!(
                    classify_region("/root/.ssh/id_rsa").read_locus,
                    LocalLocus::Machine,
                    "a grant on /root must not name the .ssh nested inside it"
                );
            });
        });
    }

    /// Freezing a trust FILE is not enough if its DIRECTORY can be replaced.
    ///
    /// With a grant on `~/.config`, every step of this was auto-approved:
    ///   rm -rf ~/.config  &&  ln -s /tmp/evil ~/.config
    /// after which safe-chains read its grants, and its `level` ceiling, out of a directory the
    /// agent controls. safe-chains classifies a path by its literal spelling and does not follow
    /// symlinks (AGENTS.md ยง0.2), which is right for classification and is exactly why the
    /// relocation has to be stopped at the point the directory is replaced.
    ///
    /// Derived from the trust files rather than from the directory nodes, so deleting those nodes
    /// makes this FAIL. The frozen-face guard cannot do that job: it enumerates declared nodes, so
    /// removing one leaves nothing to enumerate and it passes vacuously.
    #[test]
    fn a_trust_files_directory_cannot_be_destroyed_or_replaced() {
        let mut checked = 0usize;
        for node in REGIONS.nodes.iter().filter(|n| n.applies_here() && n.role.frozen == Frozen::Write) {
            let Matcher::Exact(path) = &node.matcher else { continue };
            let Some((parent, _)) = path.rsplit_once('/') else { continue };
            // `~` is DELIBERATELY not frozen (decided 2026-08-02), so it is skipped here rather
            // than passing quietly. Granting all of `~` write access is an intentional act by the
            // user, and one the docs already advise against; having done it, `rm -rf ~` is the
            // access they asked for. safe-chains does not second-guess a grant that broad. The
            // consequence to be aware of is that such a grant also permits relocating `~` itself,
            // and with it the trust root beneath it.
            if parent == "~" {
                continue;
            }
            with_grants(&[(parent, true, true)], || {
                for line in [format!("rm -rf {parent}"), format!("ln -s /tmp/evil {parent}")] {
                    assert!(
                        !crate::is_safe_command(&line),
                        "`{line}` relocates the trust root holding {path}"
                    );
                }
                // The other half of the trade-off: freezing the directory must not freeze what is
                // INSIDE it, or a `~/.config` grant would stop being useful for every other tool.
                assert!(
                    crate::is_safe_command(&format!("touch {parent}/ordinary.toml")),
                    "{parent}: freezing the directory must not freeze its contents"
                );
            });
            checked += 1;
        }
        assert!(checked >= 2, "only {checked} trust files probed โ€” the guard is vacuous");
    }

    /// The rebind split, end to end: writing INTO a trust-root directory works, replacing it does not.
    ///
    /// The first freeze of these directories used the write face, which stopped the relocation but
    /// also denied `cp x ~/.config` โ€” an ordinary thing to do in a directory you granted. Writing
    /// through a name and changing what the name refers to are different acts, and the region model
    /// now has a face for each. Both halves are asserted, because a fix that only tightened would
    /// pass a one-sided guard while leaving the over-deny in place.
    #[test]
    fn a_trust_root_directory_is_writable_into_but_not_replaceable() {
        for dir in ["~/.config", "~/.claude"] {
            with_grants(&[(dir, true, true)], || {
                for allowed in [format!("cp a.toml {dir}"), format!("mv a.toml {dir}"), format!("touch {dir}/x")] {
                    assert!(crate::is_safe_command(&allowed), "{allowed} must stay allowed");
                }
                for refused in [
                    format!("rm -rf {dir}"),
                    format!("rmdir {dir}"),
                    format!("ln -s /tmp/evil {dir}"),
                    format!("mv {dir} {dir}.bak"),
                ] {
                    assert!(!crate::is_safe_command(&refused), "{refused} relocates the trust root");
                }
            });
        }
    }

    /// A rebinding command still writes INTO a directory when the destination is a container.
    ///
    /// `rebinds_destination` describes the ENTRY `ln` creates, and `ln -t DIR a` or `ln a b DIR`
    /// puts that entry inside DIR rather than replacing it. Reading the declaration unconditionally
    /// denied `ln -t ~/.config a`, which is the same container-versus-object mistake the write face
    /// made before the rebind face existed โ€” repeated one layer up, which is why it is pinned here.
    #[test]
    fn a_rebinding_command_into_a_container_is_an_ordinary_write() {
        with_grants(&[("~/.config", true, true)], || {
            // Definitively a container: `-t`, its long spelling, and three-or-more operands.
            for allowed in [
                "ln -t ~/.config a",
                "ln --target-directory=~/.config a",
                "ln -s a b ~/.config",
            ] {
                assert!(crate::is_safe_command(allowed), "{allowed} links INTO the directory");
            }
            // Two operands stay ambiguous, and the conservative reading is the safe one: this is
            // the spelling that actually relocates the trust root.
            assert!(!crate::is_safe_command("ln -s /tmp/evil ~/.config"), "the 2-operand form rebinds");
        });
    }

    /// A `~/`-anchored node must protect the ABSOLUTE spelling of the same directory too.
    ///
    /// `resolve` deliberately does not fold `/Users/you/x` to `~/x` (pathctx leaves `~` to the
    /// classifiers), and grants cover both spellings only because `grant_matchers` generates both.
    /// Region nodes had no such treatment, so every `~/`-anchored node โ€” the credential stores under
    /// `~/Library`, `~/.config/gh`, the trust files and their directories โ€” matched one spelling
    /// only. Without a grant that failed safe, since the absolute form fell through to `unknown`.
    /// WITH a grant it did not: the grant's own absolute matcher applied to a path the shield never
    /// claimed, and the protection came off.
    #[test]
    fn a_home_anchored_node_protects_the_absolute_spelling_too() {
        let Some(home) = std::env::var_os("HOME").and_then(|h| h.into_string().ok()) else {
            return;
        };
        let cases = [
            ("~/.config", "the trust-root directory"),
            ("~/.config/safe-chains.toml", "safe-chains' own config"),
            ("~/.claude/settings.json", "the harness settings file"),
            ("~/Library/Keychains/login.keychain", "a credential store"),
        ];
        for (tilde, what) in cases {
            let absolute = tilde.replacen('~', &home, 1);
            // The same directory, so the same classification, whichever way it is spelled โ€” and
            // asserted BOTH with and without a grant, since the un-granted case failed safe on its
            // own (`unknown` denies) and hid the divergence until a grant was present.
            let compare = |when: &str| {
                let t = classify_region(tilde);
                let a = classify_region(&absolute);
                assert_eq!(a.reads_secret, t.reads_secret, "{absolute} ({when}): {what} loses its secret bit");
                assert_eq!(a.write_locus, t.write_locus, "{absolute} ({when}): {what} write face diverges");
                assert_eq!(a.rebind_locus, t.rebind_locus, "{absolute} ({when}): {what} rebind face diverges");
            };
            compare("no grant");
            with_grants(&[(tilde, true, true)], || compare("granted"));
        }
    }

    /// A freeze must survive a RAISED LEVEL, not just a grant.
    ///
    /// The freeze lives in `apply_grant`, so it only ever answered the question "can a grant widen
    /// this". The level gate is a separate question and never consulted it: `local-admin` admits
    /// `locus.local <= machine` outright, and the trust files wrote at `machine`. So a user who
    /// raised the level to `local-admin` โ€” which still refuses `/etc/sudoers` โ€” handed the agent a
    /// write to the very file that SETS the level, and with it a one-step climb to `yolo` and
    /// everything `local-admin` was refusing. Verified end to end before the fix.
    ///
    /// `yolo` is excluded deliberately, and it is not a gap: it admits everything already, so there
    /// is no privilege left to escalate to. The levels that must hold are the ones that still gate.
    #[test]
    fn a_freeze_holds_at_every_level_that_still_gates() {
        let attacks = [
            "echo x > ~/.config/safe-chains.toml",
            "echo x > ~/.claude/settings.json",
            "cp a ~/.config/safe-chains.toml",
            "rmdir ~/.config",
            "ln -s /tmp/evil ~/.config",
            "mv ~/.config ~/x",
            "rm -rf ~/.claude",
        ];
        let mut checked = 0usize;
        for name in ["local-admin", "network-admin"] {
            let level = crate::upper_level_by_name(name).expect("level exists");
            for attack in attacks {
                assert!(
                    !crate::command_verdict_at_level(attack, level).is_allowed(),
                    "`{attack}` is auto-approved at `{name}`, which can then rewrite its own ceiling"
                );
                checked += 1;
            }
        }
        assert!(checked >= 14, "only {checked} level/attack pairs probed โ€” the guard is vacuous");
        // And the gating level must still permit ordinary machine administration, or the fix has
        // simply turned `local-admin` into `developer`.
        let admin = crate::upper_level_by_name("local-admin").expect("level exists");
        for ordinary in ["echo x > /etc/nginx/nginx.conf", "cat /etc/passwd", "cp a ~/.config/other.toml"] {
            assert!(
                crate::command_verdict_at_level(ordinary, admin).is_allowed(),
                "`{ordinary}` should still be admin work at `local-admin`"
            );
        }
    }

    /// A node claiming the whole filesystem could never be NAMED by anything.
    #[test]
    fn nothing_names_a_root_that_claims_everything() {
        assert!(!at_or_below("/etc/shadow", ""), "an empty node root must not admit an absolute grant");
        assert!(!at_or_below("~/.ssh", ""));
        assert!(at_or_below("~/.ssh/known_hosts", "~/.ssh"), "a path inside the store still names it");
        assert!(!at_or_below("~/.sshfoo", "~/.ssh"), "a name-prefix neighbour does not name it");
    }

    #[test]
    fn a_grant_covers_the_subtree_however_the_path_is_spelled() {
        for spelling in ["~/projects", "~/projects/"] {
            with_grants(&[(spelling, true, true)], || {
                assert_eq!(
                    classify_region("~/projects/sibling/notes.txt").write_locus,
                    LocalLocus::Worktree,
                    "{spelling} must cover its contents"
                );
                assert_eq!(classify_region("~/projects").write_locus, LocalLocus::Worktree, "{spelling} covers the dir itself");
            });
        }
        // The component boundary still holds: a neighbour sharing a name prefix is not covered.
        with_grants(&[("~/projects", true, true)], || {
            assert_eq!(classify_region("~/projectsX/secret.txt").write_locus, LocalLocus::Machine, "~/projectsX is a different directory");
        });
    }

    /// `/etc/x` and `/private/etc/x` are one file on macOS, so they must be one classification.
    /// They were not: `/etc/shadow` was shielded and `/private/etc/shadow` read out clean, which
    /// is a bypass anyone can type. Enumerated over the real node list rather than spot-checked,
    /// so a node added under any firmlinked root is covered the day it lands.
    #[test]
    fn the_private_spelling_of_a_firmlinked_node_classifies_identically() {
        // macOS only: that is where the firmlink makes the two spellings one file. On Linux they
        // are unrelated paths and `/private/โ€ฆ` is correctly unknown โ€” `os_scope_is_load_bearing`
        // holds that end, and folding there would hand `/private/tmp` the permissive temp rung.
        let os = "macos";
        {
            with_os(os, || {
                let mut covered = 0;
                for node in REGIONS.nodes.iter().filter(|n| n.applies_here()) {
                    let Some((path, _, _)) = naming_probe(&node.matcher) else { continue };
                    if !["/etc/", "/var/", "/tmp/"].iter().any(|r| path.starts_with(r)) {
                        continue;
                    }
                    covered += 1;
                    let private = format!("/private{path}");
                    let (plain, firm) = (classify_region(&path), classify_region(&private));
                    assert_eq!(
                        (plain.read_locus, plain.write_locus, plain.reads_secret),
                        (firm.read_locus, firm.write_locus, firm.reads_secret),
                        "{os}: {path} and {private} name one file but classify differently"
                    );

                    // A SHIELD must also fire on the case variant of the firmlink prefix, because
                    // the default macOS volume is case-insensitive and `/PRIVATE/etc/shadow` is
                    // the same file. The plain fold is case-SENSITIVE on purpose (it feeds admits
                    // too), so this half is served by the shield's own extra spelling โ€” and it was
                    // missing: `/etc/SHADOW` refused while `/PRIVATE/etc/shadow` read out.
                    //
                    // Asserted for shields ONLY. An admit deliberately does not fold: on a
                    // case-sensitive volume `/PRIVATE/tmp` is a different directory and must not
                    // inherit the scratch rung.
                    if plain.reads_secret {
                        for variant in ["/PRIVATE", "/Private"] {
                            let cased = format!("{variant}{path}");
                            assert!(
                                classify_region(&cased).reads_secret,
                                "{os}: {cased} is {path} on a case-insensitive volume, but the shield missed it"
                            );
                        }
                    }
                }
                assert!(covered >= 3, "{os}: guard covered only {covered} firmlinked nodes โ€” vacuous");
            });
        }
    }

    #[test]
    fn a_grant_reaches_a_credential_store_only_when_it_names_it() {
        for os in ["macos", "linux"] {
            with_os(os, || {
                let mut covered = 0;
                for node in REGIONS.nodes.iter().filter(|n| n.applies_here() && n.role.reads_secret) {
                    let probes: Vec<_> = [naming_probe(&node.matcher), absolute_segment_probe(&node.matcher)]
                        .into_iter()
                        .flatten()
                        .collect();
                    if probes.is_empty() {
                        continue;
                    }
                    for (path, naming, parent) in probes {
                    assert!(
                        base_region(&path).reads_secret,
                        "{os}: probe {path} does not reach the secret node it was built from"
                    );
                    with_grants(&[(naming.as_str(), true, true)], || {
                        assert_eq!(
                            classify_region(&path).read_locus,
                            LocalLocus::WorktreeTrusted,
                            "{os}: a grant naming {naming} must reach {path}"
                        );
                    });
                    with_grants(&[(parent.as_str(), true, true)], || {
                        assert_eq!(
                            classify_region(&path).read_locus,
                            LocalLocus::Machine,
                            "{os}: a grant on the parent {parent} must NOT reach the secret at {path}"
                        );
                    });
                    covered += 1;
                    }
                }
                assert!(covered > 10, "{os}: only {covered} credential stores probed - the guard has gone vacuous");
            });
        }
    }

    /// Grants borrowed from another tool's config never name a credential store, however specific
    /// they are. A `Read(~/.ssh/**)` rule answers Claude's permission prompt; it does not say the
    /// user wants every command touching `~/.ssh` auto-approved here.
    #[test]
    fn a_derived_grant_never_names_a_credential_store() {
        for os in ["macos", "linux"] {
            with_os(os, || {
                let mut covered = 0;
                for node in REGIONS.nodes.iter().filter(|n| n.applies_here() && n.role.reads_secret) {
                    let Some((path, naming, _)) = naming_probe(&node.matcher) else {
                        continue;
                    };
                    with_derived_grants(&[(naming.as_str(), true, false)], || {
                        assert_eq!(
                            classify_region(&path).read_locus,
                            LocalLocus::Machine,
                            "{os}: a derived grant on {naming} must not reach {path}"
                        );
                    });
                    // The same grant written by the user in safe-chains' own config DOES reach it,
                    // so this guard is testing the source and not merely re-testing the naming test.
                    with_grants(&[(naming.as_str(), true, false)], || {
                        assert_eq!(classify_region(&path).read_locus, LocalLocus::WorktreeTrusted);
                    });
                    covered += 1;
                }
                assert!(covered > 10, "{os}: only {covered} stores probed - the guard has gone vacuous");
            });
        }
    }

    /// Every frozen face, enumerated over the region table, each checked on the face it froze.
    ///
    /// Granting these away forfeits the ability to enforce anything afterwards, so no grant opens
    /// them however it is spelled. Which FACE that means differs by role, and conflating the two
    /// was the over-deny this split exists to fix: `frozen = "write"` (a trust file) must refuse
    /// every write, while `frozen = "rebind"` (the directory it lives in) must still ACCEPT an
    /// ordinary write and refuse only the replacement. Reads are never frozen: `/etc/passwd` is
    /// world-readable and safe-chains' own config is readable already.
    #[test]
    fn no_grant_opens_a_frozen_face() {
        for os in ["macos", "linux"] {
            with_os(os, || {
                let (mut covered, mut rebind_only) = (0, 0);
                for node in REGIONS.nodes.iter().filter(|n| n.applies_here()) {
                    let role = node.role;
                    let system = role.write_locus >= LocalLocus::SystemIntegrity;
                    if role.frozen == Frozen::Nothing && !system {
                        continue;
                    }
                    let Some((path, naming, parent)) = naming_probe(&node.matcher) else {
                        continue;
                    };
                    for grant in [naming.as_str(), parent.as_str(), "~/", "/"] {
                        with_grants(&[(grant, true, true)], || {
                            let r = classify_region(&path);
                            // The rebind face is frozen for BOTH kinds: a write freeze implies it.
                            assert!(
                                r.rebind_locus > LocalLocus::Worktree,
                                "{os}: grant {grant} opens the frozen rebind at {path}"
                            );
                            if role.frozen == Frozen::Write || system {
                                assert!(
                                    r.write_locus > LocalLocus::Worktree,
                                    "{os}: grant {grant} opens the frozen write at {path}"
                                );
                            }
                        });
                    }
                    // The other half of the split, asserted only for the grant that actually
                    // REACHES the node. A broad `~/` grant does not reach `~/.config` at all โ€” the
                    // dotfile rule stops it โ€” so it proves nothing about the freeze either way.
                    if role.frozen == Frozen::Rebind {
                        with_grants(&[(naming.as_str(), true, true)], || {
                            assert!(
                                classify_region(&path).write_locus <= LocalLocus::Worktree,
                                "{os}: a rebind-only freeze must still allow writing into {path}"
                            );
                        });
                    }
                    covered += 1;
                    if role.frozen == Frozen::Rebind {
                        rebind_only += 1;
                    }
                }
                assert!(covered > 5, "{os}: only {covered} frozen nodes probed - the guard has gone vacuous");
                assert!(rebind_only >= 2, "{os}: {rebind_only} rebind-only nodes - the split is untested");
            });
        }
    }

    #[test]
    fn shields_fold_case_on_macos_so_a_case_variant_cannot_evade_them() {
        // On APFS (case-insensitive) a case-variant names the SAME file, so every protection โ€”
        // credential stores AND the `.git`/`.envrc` write-freeze โ€” must fire on the variant.
        with_os("macos", || {
            assert!(classify_region("~/.AWS/credentials").reads_secret, ".AWS folds to the .aws secret");
            assert!(classify_region("~/.SSH/id_rsa").reads_secret, ".SSH folds to the .ssh secret");
            assert_eq!(classify_region("~/.AWS/credentials").read_locus, LocalLocus::Machine);
            assert_eq!(classify_region("/etc/Master.Passwd").read_locus, LocalLocus::Machine, "system secret folds");
            // the agent-injectable one: a case-variant .git/.envrc WRITE stays frozen
            assert!(classify_region(".GIT/hooks/pre-commit").write_locus > LocalLocus::Worktree, ".GIT write frozen");
            assert!(classify_region(".Git/hooks/pre-commit").write_locus > LocalLocus::Worktree, "mixed-case .Git frozen");
            assert!(classify_region(".ENVRC").write_locus > LocalLocus::Worktree, ".ENVRC write frozen");
            // A grant NAMING the folded spelling reaches it: on this filesystem `~/.AWS` is the
            // very same directory as `~/.aws`, so someone who granted one granted the other.
            with_grants(&[("~/.AWS/", true, false)], || {
                assert_eq!(classify_region("~/.AWS/credentials").read_locus, LocalLocus::WorktreeTrusted, "a grant naming the folded secret reaches it");
            });
            // A grant that does NOT name it still cannot reach through the fold.
            with_grants(&[("~/", true, false)], || {
                assert_eq!(classify_region("~/.AWS/credentials").read_locus, LocalLocus::Machine, "a broad grant cannot reach a folded secret");
            });
        });
    }

    #[test]
    fn case_folding_is_macos_only_so_linux_keeps_distinct_paths() {
        // On a case-sensitive fs `.GIT` and `~/.AWS` are DIFFERENT files, not the shielded ones โ€”
        // folding there would be a false-deny. The canonical spelling is shielded on every OS.
        with_os("linux", || {
            assert_eq!(classify_region(".GIT/hooks/pre-commit").write_locus, LocalLocus::Worktree, "linux: .GIT is an ordinary worktree path");
            assert!(!classify_region("~/.AWS/credentials").reads_secret, "linux: .AWS is not the .aws secret");
        });
        for os in ["macos", "linux"] {
            assert!(with_os(os, || classify_region("~/.aws/credentials").reads_secret), "{os}: canonical .aws shielded");
            assert!(with_os(os, || classify_region(".git/hooks/pre-commit").write_locus > LocalLocus::Worktree), "{os}: canonical .git frozen");
        }
    }

    #[test]
    fn admit_nodes_never_fold_so_a_case_variant_is_not_widened() {
        // Folding an ADMIT would be fail-OPEN on a case-sensitive volume (`/TMP` โ‰  `/tmp`). So even
        // on macOS `/TMP` is NOT admitted as scratch โ€” it fails closed to unknown.
        with_os("macos", || {
            assert!(classify_region("/tmp/x").write_locus <= LocalLocus::Worktree, "/tmp is scratch (admitted)");
            assert_eq!(classify_region("/TMP/x").write_locus, LocalLocus::Machine, "/TMP is not folded into the scratch admit");
        });
    }

    #[test]
    fn safe_chains_config_is_read_ok_write_denied_and_ungrantable() {
        let cfg = "~/.config/safe-chains.toml";
        assert!(classify_region(cfg).read_locus <= LocalLocus::WorktreeTrusted, "read is fine");
        // `system-integrity`, not `machine`, and the exact rung is the point rather than an
        // implementation detail: `machine` is admitted outright by `local-admin`, so the file that
        // SETS the level was writable at a level it was supposed to bound. The rung above is the
        // one no level below yolo admits.
        assert_eq!(classify_region(cfg).write_locus, LocalLocus::SystemIntegrity, "write denied");
        // even a broad ~/ grant cannot widen the write (the trust root is pinned)
        with_grants(&[("~/", true, true)], || {
            assert_eq!(classify_region(cfg).write_locus, LocalLocus::SystemIntegrity, "grant can't unlock the config write");
            assert!(classify_region(cfg).read_locus <= LocalLocus::WorktreeTrusted);
        });
    }

    #[test]
    fn a_grant_does_not_widen_hidden_files_or_system_secrets() {
        with_grants(&[("~/", true, true)], || {
            assert_eq!(classify_region("~/projects/foo/main.rs").write_locus, LocalLocus::Worktree);
            // Credential dotfiles are NOT swept up by a broad grant. They are held by the shield
            // NAMING them, not by being hidden: this release made ordinary home dotfiles readable
            // (`~/.zshrc`, `~/.gitconfig`), so "hidden" stopped being the test and the list below
            // is exactly the set the shield declares.
            for p in ["~/.git-credentials", "~/.npmrc", "~/.config/gh/hosts.yml", "~/.pgpass"] {
                assert_eq!(classify_region(p).read_locus, LocalLocus::Machine, "shielded, not widened: {p}");
            }
            // `.SSH` is the CASE variant, so it depends on the filesystem and must be asserted per
            // OS rather than once. macOS folds shields (the default volume is case-insensitive, so
            // it is the same directory); Linux does not, where `~/.SSH` is a genuinely different
            // directory and reads as the ordinary dotfile it is. Asserting the macOS answer
            // everywhere is what broke on CI.
            assert_eq!(
                with_os("macos", || classify_region("~/.SSH/id_rsa").read_locus),
                LocalLocus::Machine,
                "macos: the case variant is the same directory, so the shield must fold onto it"
            );
            assert_eq!(
                with_os("linux", || classify_region("~/.SSH/id_rsa").read_locus),
                LocalLocus::User,
                "linux: ~/.SSH is not ~/.ssh, and an unlisted dotfile is an ordinary home read"
            );
        });
        // a `/` grant cannot reach a system credential store (un-grantable shield)
        with_grants(&[("/", true, true)], || {
            assert_eq!(classify_region("/etc/ssl/private/server.key").read_locus, LocalLocus::Machine);
            assert_eq!(with_os("linux", || classify_region("/etc/shadow").read_locus), LocalLocus::Machine);
        });
        // an EXPLICIT dotdir grant still reaches its non-hidden contents
        with_grants(&[("~/.runner-scripts/", true, true)], || {
            assert_eq!(classify_region("~/.runner-scripts/deploy.sh").write_locus, LocalLocus::Worktree);
        });
        // macOS ~/Library credential stores are NOT dot-prefixed, so the dotfile rule can't catch
        // them under `grant ~/` โ€” the shields must (un-grantable, like the dotdirs).
        with_grants(&[("~/", true, true)], || {
            for p in [
                "~/Library/Keychains/login.keychain-db",
                "~/Library/Cookies/Cookies.binarycookies",
                "~/Library/Application Support/Firefox/Profiles/x.default/logins.json",
                "~/Library/Application Support/Google/Chrome/Default/Login Data",
                "~/.config/git/credentials",
            ] {
                assert_eq!(with_os("macos", || classify_region(p).read_locus), LocalLocus::Machine, "shield: {p}");
            }
        });
    }

    #[test]
    fn a_broad_grant_never_reaches_a_secret_carveout() {
        with_grants(&[("~/", true, true)], || {
            let r = classify_region("~/.ssh/id_rsa");
            assert_eq!(r.read_locus, LocalLocus::Machine, "a ~/ grant does not name ~/.ssh, so it does not reach it");
            assert!(r.reads_secret);
        });
    }

    /// A read grant that NAMES a credential store clears the shield for it โ€” the other half of the
    /// rule the test above pins.
    ///
    /// `best_grant` already refused to let a broad grant reach a store, but `apply_grant` then kept
    /// `reads_secret` regardless, so a NAMING grant moved the locus and nothing else. Since no level
    /// below yolo admits `secret ยท reads`, the read still refused: the first lever a user reaches
    /// for did nothing, and nothing said why. Measured before the fix โ€”
    /// `[[grant]] path = "~/.ssh", read = true` left `cat ~/.ssh/id_rsa` denied.
    ///
    /// Only the READ face. `read = true` says nothing about writing, and the write faces stay behind
    /// `write_grantable`, where `pinned` keeps its blanket refusal.
    #[test]
    fn a_grant_that_names_a_secret_store_opens_it_for_reading() {
        with_grants(&[("~/.ssh", true, false)], || {
            let r = classify_region("~/.ssh/id_rsa");
            assert!(!r.reads_secret, "the grant named ~/.ssh, so the shield yields to it");
            assert!(r.read_locus <= LocalLocus::WorktreeTrusted, "and the locus is granted too");
        });

        // A grant one level INSIDE the store also names it โ€” the rule is at-or-below, so granting
        // `~/.ssh/config` must not accidentally clear `~/.ssh/id_rsa` beside it.
        with_grants(&[("~/.ssh/config", true, false)], || {
            assert!(!classify_region("~/.ssh/config").reads_secret, "the named file opens");
            assert!(
                classify_region("~/.ssh/id_rsa").reads_secret,
                "a sibling the grant did not name stays shielded"
            );
        });

        // A WRITE grant does not clear the read shield, and does not open the write face of a
        // store either.
        with_grants(&[("~/.ssh", false, true)], || {
            let r = classify_region("~/.ssh/id_rsa");
            assert!(r.reads_secret, "write = true is not a statement about reading");
        });
    }

    #[test]
    fn grant_takes_effect_end_to_end() {
        with_grants(&[("~/projects/", true, true)], || {
            assert!(crate::is_safe_command("cat ~/projects/sibling/notes.txt"));
            assert!(crate::is_safe_command("cp ./a ~/projects/sibling/b"));
            // a redirect write honors the grant too (not just engine writers)
            assert!(crate::is_safe_command("echo hi > ~/projects/sibling/out.txt"));
        });
    }

    #[test]
    fn a_home_grant_matches_both_tilde_and_absolute_spellings() {
        let Some(home) = std::env::var_os("HOME").and_then(|h| h.into_string().ok()) else {
            return;
        };
        with_grants(&[("~/work/", true, true)], || {
            assert!(classify_region("~/work/a.txt").write_locus == LocalLocus::Worktree);
            assert!(classify_region(&format!("{home}/work/a.txt")).write_locus == LocalLocus::Worktree);
        });
    }

    /// Provenance discipline (mirrors `researched_version`): no node may ship without a `note`
    /// and a `researched` date, and every referenced role must resolve.
    #[test]
    fn every_region_carries_provenance_and_a_valid_role() {
        let src = include_str!("../../../regions/default.toml");
        let file: RegionsFile = toml::from_str(src).expect("valid TOML");
        for r in &file.region {
            assert!(!r.note.trim().is_empty(), "region `{}` is missing a note", r.path);
            assert!(!r.researched.trim().is_empty(), "region `{}` is missing a researched date", r.path);
            assert!(file.role.contains_key(&r.role), "region `{}` names undefined role `{}`", r.path, r.role);
        }
        assert!(file.region.len() > 10, "region set unexpectedly small ({})", file.region.len());
    }
}