Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
83 changes: 82 additions & 1 deletion src/tcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -109,6 +113,32 @@ impl fmt::Display for TccError {
}
}

fn tcc_open_access_denied_hint(path: &Path, source: &str) -> Option<String> {
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,
Expand Down Expand Up @@ -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]
Expand Down