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
//! `tm doctor` macOS TCC responsibility-taint check (issue #2997).
//!
//! Why: on macOS, an interactive `claude` launched inside a trusty-mpm-managed
//! tmux pane trips the TCC gate (App-Data / `RemovableVolumes`), and tccd walks
//! the pane's responsibility chain `claude → zsh → shared tmux server` and
//! attributes the prompt to the shared server. #3037 + the launcher-disclaim
//! extension route the pane's `claude` through `internal-spawn-disclaimed` so
//! it becomes its OWN responsible process — but that whole mechanism was
//! previously INVISIBLE to `tm doctor` (the diagnosis noted it had no TCC line
//! at all). This probe surfaces whether the disclaim is actually in force so
//! the taint class is diagnosable instead of silent.
//! What: [`check_tcc_taint`] gathers three facts — is this macOS, does the
//! private disclaim SPI resolve, and is the `TM_DISABLE_SPAWN_DISCLAIM` escape
//! hatch set — and folds them via the pure [`build_tcc_taint_check`]. Read-only
//! and instantaneous (no `log show`, no process walk): it reports the disclaim
//! wiring's status, not a live TCC event scan.
//! Test: `tcc_taint_ok_when_disclaim_active`, `tcc_taint_warn_when_disabled`,
//! `tcc_taint_warn_when_spi_absent`, `tcc_taint_ok_on_non_macos`.
use crate::core::doctor::{CheckStatus, DoctorCheck};
/// Probe whether managed panes spawn `claude` with TCC responsibility
/// disclaimed (issue #2997).
///
/// Why: see the module docs — makes the disclaim mechanism's state visible in
/// `tm doctor` so an operator seeing a "tmux"/"trusty-mpm would like to
/// access…" prompt can tell whether the attribution fix is active.
/// What: reads the three real inputs (macOS build, disclaim SPI availability
/// via [`crate::core::spawn_disclaim::disclaim_spi_available`], escape-hatch
/// state via [`crate::core::spawn_disclaim::disclaim_disabled_by_env`]) and
/// delegates the verdict to [`build_tcc_taint_check`].
/// Test: covered indirectly by the `build_tcc_taint_check` unit tests (the pure
/// verdict) — this wrapper only wires in the live inputs.
pub fn check_tcc_taint() -> DoctorCheck {
let is_macos = cfg!(target_os = "macos");
let spi_available = crate::core::spawn_disclaim::disclaim_spi_available();
let disabled_by_env = crate::core::spawn_disclaim::disclaim_disabled_by_env();
build_tcc_taint_check(is_macos, spi_available, disabled_by_env)
}
/// Fold the disclaim-state facts into a [`DoctorCheck`] (pure).
///
/// Why: keeping the verdict logic pure makes every branch — active, disabled,
/// SPI-absent, non-macOS — unit-testable without a real macOS or process env.
/// What: `Ok` on non-macOS (no TCC exists there). On macOS: `Warn` when the
/// escape hatch is set (disclaim off), `Warn` when the private SPI does not
/// resolve (can't disclaim on this OS), else `Ok` with the scope caveat that a
/// FIRST prompt may still appear — now attributed to Claude Code itself, which
/// the operator can grant once. Never `Fail`: a missing disclaim is a degraded
/// attribution state, not a broken stack.
/// Test: `tcc_taint_ok_when_disclaim_active`, `tcc_taint_warn_when_disabled`,
/// `tcc_taint_warn_when_spi_absent`, `tcc_taint_ok_on_non_macos`.
fn build_tcc_taint_check(
is_macos: bool,
spi_available: bool,
disabled_by_env: bool,
) -> DoctorCheck {
if !is_macos {
return DoctorCheck::new(
"tcc_taint",
CheckStatus::Ok,
"not applicable on this platform (macOS TCC only)",
);
}
if disabled_by_env {
return DoctorCheck::new(
"tcc_taint",
CheckStatus::Warn,
"TM_DISABLE_SPAWN_DISCLAIM is set — managed panes launch `claude` WITHOUT the \
TCC disclaim, so its data-access prompts are attributed to the shared tmux \
server (issue #2997). Unset it to re-enable the disclaim.",
);
}
if !spi_available {
return DoctorCheck::new(
"tcc_taint",
CheckStatus::Warn,
"the macOS responsibility-disclaim SPI did not resolve on this build — managed \
panes cannot disclaim TCC responsibility, so `claude`'s data-access prompts may \
be attributed to the shared tmux server (issue #2997).",
);
}
DoctorCheck::new(
"tcc_taint",
CheckStatus::Ok,
"managed panes spawn `claude` with TCC responsibility disclaimed (issue #2997 \
attribution fix active). Note: a first-time data-access prompt may still appear — \
now attributed to Claude Code itself, not tmux/trusty-mpm — and can be granted once.",
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tcc_taint_ok_when_disclaim_active() {
let c = build_tcc_taint_check(true, true, false);
assert_eq!(c.status, CheckStatus::Ok);
assert_eq!(c.name, "tcc_taint");
assert!(c.message.contains("attribution fix active"));
}
#[test]
fn tcc_taint_warn_when_disabled() {
// Escape hatch set → disclaim off → warn, even though the SPI is present.
let c = build_tcc_taint_check(true, true, true);
assert_eq!(c.status, CheckStatus::Warn);
assert!(c.message.contains("TM_DISABLE_SPAWN_DISCLAIM"));
}
#[test]
fn tcc_taint_warn_when_spi_absent() {
let c = build_tcc_taint_check(true, false, false);
assert_eq!(c.status, CheckStatus::Warn);
assert!(c.message.contains("SPI"));
}
#[test]
fn tcc_taint_ok_on_non_macos() {
// Off macOS there is no TCC — never warn regardless of the other inputs.
let c = build_tcc_taint_check(false, false, true);
assert_eq!(c.status, CheckStatus::Ok);
assert!(c.message.contains("not applicable"));
}
#[test]
fn tcc_taint_is_advisory_only() {
// This probe must never Fail — a missing disclaim is degraded, not broken.
for (macos, spi, disabled) in [
(true, true, false),
(true, true, true),
(true, false, false),
(true, false, true),
(false, false, false),
] {
assert_ne!(
build_tcc_taint_check(macos, spi, disabled).status,
CheckStatus::Fail
);
}
}
}