From a3bfad34d30b43fe0566f9c9c3b8c9c77e55426e Mon Sep 17 00:00:00 2001 From: glitch418x Date: Sat, 21 Feb 2026 02:09:04 +0300 Subject: [PATCH] fix(tcc): add FDA hint for TCC authorization-denied opens --- README.md | 8 ++++++ src/tcc.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d981734..fc1b42d 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,14 @@ On macOS 10.14+, System Integrity Protection restricts direct writes to TCC data In practice, the **user database** is writable regardless of SIP. The **system database** requires running with `sudo` (works for most operations on recent macOS). +## Troubleshooting + +### Full Disk Access (sqlite open authorization denied) + +If you see an authorization-denied error opening `TCC.db`, grant **Full Disk Access** to the terminal app running `tccutil-rs` (for example Terminal, iTerm, Ghostty, or VS Code's integrated terminal), then fully quit and reopen that app before retrying. + +`sudo` does not bypass TCC privacy protections. + ## Comparison | | Apple `tccutil` | [tccutil.py](https://github.com/jacobsalmela/tccutil) | `tccutil-rs` | diff --git a/src/tcc.rs b/src/tcc.rs index b852fdd..879b483 100644 --- a/src/tcc.rs +++ b/src/tcc.rs @@ -80,7 +80,11 @@ impl fmt::Display for TccError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TccError::DbOpen { path, source } => { - write!(f, "Failed to open {}: {}", path.display(), source) + write!(f, "Failed to open {}: {}", path.display(), source)?; + if let Some(hint) = tcc_open_access_denied_hint(path, source) { + write!(f, "\n\n{}", hint)?; + } + Ok(()) } TccError::NotFound { service, client } => { write!( @@ -109,6 +113,32 @@ impl fmt::Display for TccError { } } +fn tcc_open_access_denied_hint(path: &Path, source: &str) -> Option { + if !is_tcc_db_path(path) { + return None; + } + + let source_lower = source.to_lowercase(); + let is_open_denied = source_lower.contains("authorization denied") + || source_lower.contains("open authorization denied") + || source_lower.contains("not authorized"); + if !is_open_denied { + return None; + } + + Some( + "macOS blocked access to TCC.db (Full Disk Access is required for terminal apps).\n\ + Grant Full Disk Access to the app launching this command (Terminal, iTerm, Ghostty, VS Code, etc.), then fully quit and reopen that app before retrying.\n\ + `sudo` does not bypass TCC privacy protections." + .to_string(), + ) +} + +fn is_tcc_db_path(path: &Path) -> bool { + path == Path::new("/Library/Application Support/com.apple.TCC/TCC.db") + || path.ends_with("Library/Application Support/com.apple.TCC/TCC.db") +} + #[derive(Debug)] pub struct TccEntry { pub service_raw: String, @@ -852,6 +882,57 @@ mod tests { assert_eq!(auth_value_display(-1), "unknown(-1)"); } + // ── DB open authorization hint mapping ─────────────────────────── + + #[test] + fn db_open_auth_denied_on_user_tcc_db_includes_fda_hint() { + let err = TccError::DbOpen { + path: PathBuf::from("/Users/test/Library/Application Support/com.apple.TCC/TCC.db"), + source: "opening database: authorization denied".to_string(), + }; + + let rendered = err.to_string(); + assert!(rendered.contains("Failed to open")); + assert!(rendered.contains("Full Disk Access")); + assert!(rendered.contains("Terminal, iTerm, Ghostty, VS Code")); + assert!(rendered.contains("fully quit and reopen")); + assert!(rendered.contains("`sudo` does not bypass TCC")); + } + + #[test] + fn db_open_auth_denied_on_system_tcc_db_includes_fda_hint() { + let err = TccError::DbOpen { + path: PathBuf::from("/Library/Application Support/com.apple.TCC/TCC.db"), + source: "Open authorization denied".to_string(), + }; + + let rendered = err.to_string(); + assert!(rendered.contains("Full Disk Access")); + } + + #[test] + fn db_open_auth_denied_on_non_tcc_path_does_not_include_hint() { + let err = TccError::DbOpen { + path: PathBuf::from("/tmp/not-tcc.db"), + source: "opening database: authorization denied".to_string(), + }; + + let rendered = err.to_string(); + assert!(!rendered.contains("Full Disk Access")); + assert!(!rendered.contains("`sudo` does not bypass TCC")); + } + + #[test] + fn db_open_non_auth_error_on_tcc_path_does_not_include_hint() { + let err = TccError::DbOpen { + path: PathBuf::from("/Library/Application Support/com.apple.TCC/TCC.db"), + source: "unable to open database file".to_string(), + }; + + let rendered = err.to_string(); + assert!(!rendered.contains("Full Disk Access")); + } + // ── Compact path display ────────────────────────────────────────── #[test]