use std::process::Command;
use crate::error::{AppError, Result};
const SERVICE: &str = "Claude Code-credentials";
fn account() -> Option<String> {
std::env::var("USER").ok().filter(|u| !u.is_empty())
}
const ERR_SEC_ITEM_NOT_FOUND: i32 = 44;
pub fn read_raw() -> Result<Option<String>> {
let mut cmd = Command::new("/usr/bin/security");
cmd.args(["find-generic-password", "-s", SERVICE, "-w"]);
if let Some(acct) = account() {
cmd.args(["-a", &acct]);
}
let out = cmd
.output()
.map_err(|e| AppError::Other(format!("could not run `security`: {e}")))?;
if !out.status.success() {
if out.status.code() == Some(ERR_SEC_ITEM_NOT_FOUND) {
return Ok(None);
}
let detail = String::from_utf8_lossy(&out.stderr);
let detail = detail.trim();
return Err(AppError::Credentials(format!(
"could not read the Claude credentials from the macOS Keychain \
(security exited {}): {}. If the login Keychain is locked, unlock \
it and retry; if access was denied, allow ai-usagebar when prompted.",
out.status.code().unwrap_or(-1),
if detail.is_empty() {
"no detail"
} else {
detail
}
)));
}
let value = String::from_utf8(out.stdout)
.map_err(|e| AppError::Other(format!("Keychain value was not UTF-8: {e}")))?;
let value = value.trim_end_matches('\n').to_string();
if value.is_empty() {
Ok(None)
} else {
Ok(Some(value))
}
}
pub fn write_raw(json: &str) -> Result<()> {
let mut cmd = Command::new("/usr/bin/security");
cmd.args(["add-generic-password", "-U", "-s", SERVICE]);
if let Some(acct) = account() {
cmd.args(["-a", &acct]);
}
cmd.args(["-w", json]);
let out = cmd
.output()
.map_err(|e| AppError::Other(format!("could not run `security`: {e}")))?;
if out.status.success() {
return Ok(());
}
let detail = String::from_utf8_lossy(&out.stderr);
let detail = detail.trim();
Err(AppError::Credentials(format!(
"failed to update the Claude credentials in the macOS Keychain \
(security exited {}): {}",
out.status.code().unwrap_or(-1),
if detail.is_empty() {
"no detail"
} else {
detail
}
)))
}