#[cfg(target_os = "macos")]
pub(crate) fn read_generic_password(service: &str, account: Option<&str>) -> Option<String> {
let mut cmd = std::process::Command::new("security");
cmd.arg("find-generic-password").arg("-s").arg(service);
if let Some(account) = account {
cmd.arg("-a").arg(account);
}
cmd.arg("-w");
let output = cmd.output().ok()?;
if !output.status.success() {
return None;
}
let value = String::from_utf8(output.stdout).ok()?;
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn read_generic_password(_service: &str, _account: Option<&str>) -> Option<String> {
None
}
#[cfg(target_os = "macos")]
pub(crate) fn parse_account(attributes_output: &str) -> Option<String> {
for line in attributes_output.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("\"acct\"<blob>=\"")
&& let Some(value) = rest.strip_suffix('"')
{
return Some(value.to_string());
}
}
None
}
#[cfg(target_os = "macos")]
pub(crate) fn find_account(service: &str) -> Option<String> {
let output = std::process::Command::new("security")
.arg("find-generic-password")
.arg("-s")
.arg(service)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
parse_account(&stdout).or_else(|| {
let stderr = String::from_utf8_lossy(&output.stderr);
parse_account(&stderr)
})
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn find_account(_service: &str) -> Option<String> {
None
}
#[cfg(target_os = "macos")]
pub(crate) fn write_generic_password(service: &str, account: &str, value: &str) -> bool {
std::process::Command::new("security")
.arg("add-generic-password")
.arg("-U")
.arg("-s")
.arg(service)
.arg("-a")
.arg(account)
.arg("-w")
.arg(value)
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn write_generic_password(_service: &str, _account: &str, _value: &str) -> bool {
false
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
fn parse_account_extracts_acct_field() {
let out = "keychain: \"/Users/x/Library/Keychains/login.keychain-db\"\n \
0x00000007 <blob>=\"Claude Code-credentials\"\n \
\"acct\"<blob>=\"archer\"\n \
\"svce\"<blob>=\"Claude Code-credentials\"\n";
assert_eq!(parse_account(out).as_deref(), Some("archer"));
}
#[test]
fn parse_account_none_when_absent() {
assert_eq!(parse_account("nothing here\n \"svce\"<blob>=\"x\""), None);
}
}