Agent skill
keyring
Cross-platform secure credential storage using system keychains
Install this agent skill to your Project
npx add-skill https://github.com/johnlindquist/script-kit-next/tree/main/.opencode/skill/keyring
SKILL.md
keyring
Cross-platform library for securely storing and retrieving passwords/credentials using the operating system's native credential storage.
Overview
keyring provides a unified API to interact with platform-specific secure credential stores:
- Each credential is identified by a
(service, user)pair - Credentials are stored securely in the OS keychain, encrypted at rest
- The library abstracts platform differences behind the
Entrytype
Key Types
Entry
The main type for interacting with credentials:
use keyring::Entry;
// Create an entry for a service/user combination
let entry = keyring::Entry::new("com.myapp.service", "username")?;
Error
Non-exhaustive enum covering all failure modes:
| Variant | Description |
|---|---|
NoEntry |
No credential exists for this entry |
BadEncoding(Vec<u8>) |
Password is not valid UTF-8 (raw bytes attached) |
TooLong(String, u32) |
Attribute exceeded platform length limit |
Invalid(String, String) |
Invalid attribute (name, reason) |
Ambiguous(Vec<Credential>) |
Multiple matching credentials found |
PlatformFailure(Box<dyn Error>) |
Underlying platform error |
NoStorageAccess(Box<dyn Error>) |
Cannot access credential store (locked?) |
Result
Type alias: Result<T, keyring::Error>
Usage in script-kit-gpui
The env.rs module uses keyring for secure secret storage in the EnvPrompt:
// Service identifier for all script-kit secrets
const KEYRING_SERVICE: &str = "com.scriptkit.env";
// Get a secret from keyring
pub fn get_secret(key: &str) -> Option<String> {
let entry = keyring::Entry::new(KEYRING_SERVICE, key);
match entry {
Ok(entry) => match entry.get_password() {
Ok(value) => Some(value),
Err(keyring::Error::NoEntry) => None, // Key doesn't exist yet
Err(e) => {
// Log error, return None
None
}
},
Err(e) => None, // Entry creation failed
}
}
// Set a secret in keyring
pub fn set_secret(key: &str, value: &str) -> Result<(), String> {
let entry = keyring::Entry::new(KEYRING_SERVICE, key)
.map_err(|e| format!("Failed to create keyring entry: {}", e))?;
entry
.set_password(value)
.map_err(|e| format!("Failed to store secret: {}", e))?;
Ok(())
}
// Delete a secret from keyring
pub fn delete_secret(key: &str) -> Result<(), String> {
let entry = keyring::Entry::new(KEYRING_SERVICE, key)
.map_err(|e| format!("Failed to create keyring entry: {}", e))?;
entry
.delete_credential()
.map_err(|e| format!("Failed to delete secret: {}", e))?;
Ok(())
}
EnvPrompt Flow
- On prompt display,
check_keyring_and_auto_submit()checks if secret exists - If found, auto-submits the stored value (user sees nothing)
- If not found, prompts user for input
- On submit, if
secret: true, stores value in keyring viaset_secret()
CRUD Operations
Create/Update: set_password
let entry = Entry::new("my.service", "user")?;
entry.set_password("secret_value")?;
For binary data (non-UTF8):
entry.set_secret(&[0x01, 0x02, 0x03])?;
Read: get_password
let entry = Entry::new("my.service", "user")?;
match entry.get_password() {
Ok(password) => println!("Got: {}", password),
Err(keyring::Error::NoEntry) => println!("Not found"),
Err(e) => eprintln!("Error: {}", e),
}
For binary data:
let bytes: Vec<u8> = entry.get_secret()?;
Delete: delete_credential
let entry = Entry::new("my.service", "user")?;
entry.delete_credential()?; // Returns NoEntry if doesn't exist
Platform Backends
| Platform | Backend | Feature Flag |
|---|---|---|
| macOS | Keychain Services | apple-native |
| iOS | Keychain Services | apple-native |
| Windows | Windows Credential Manager | windows-native |
| Linux | Secret Service (DBus) | sync-secret-service or async-secret-service |
| Linux | kernel keyutils | linux-native |
| Linux | keyutils + Secret Service | linux-native-sync-persistent |
Cargo.toml Configuration
script-kit-gpui uses:
keyring = "3"
For explicit platform features:
[target.'cfg(target_os = "macos")'.dependencies]
keyring = { version = "3", features = ["apple-native"] }
[target.'cfg(target_os = "windows")'.dependencies]
keyring = { version = "3", features = ["windows-native"] }
[target.'cfg(target_os = "linux")'.dependencies]
keyring = { version = "3", features = ["sync-secret-service"] }
Advanced Features
Target-Based Entries
Distinguish entries with same service/user:
let entry = Entry::new_with_target("production", "my.service", "user")?;
Attributes
Get/set metadata on credentials (platform-dependent):
use std::collections::HashMap;
let attrs = entry.get_attributes()?;
let mut updates = HashMap::new();
updates.insert("label", "My App Secret");
entry.update_attributes(&updates)?;
Custom Credential Builders
Replace the default credential builder for custom storage backends:
use keyring::{set_default_credential_builder, Entry};
// Custom builder that uses your storage
set_default_credential_builder(my_custom_builder);
// Now Entry::new() uses your builder
let entry = Entry::new("service", "user")?;
Anti-patterns
Don't ignore NoEntry errors
// BAD: Crashes if key doesn't exist
let password = entry.get_password().unwrap();
// GOOD: Handle missing keys gracefully
match entry.get_password() {
Ok(pw) => use_password(pw),
Err(keyring::Error::NoEntry) => prompt_user(),
Err(e) => handle_error(e),
}
Don't create entries in hot paths
// BAD: Creates entry on every call
fn check_auth() -> bool {
let entry = Entry::new("svc", "user").unwrap();
entry.get_password().is_ok()
}
// GOOD: Create once, reuse
struct AuthChecker {
entry: Entry,
}
Don't assume UTF-8 passwords
Third-party apps may store binary data:
// BAD: Fails if password isn't UTF-8
let pw = entry.get_password()?;
// GOOD: Use get_secret for unknown sources
match entry.get_password() {
Ok(pw) => use_password(pw),
Err(keyring::Error::BadEncoding(bytes)) => {
// Handle raw bytes
}
Err(e) => handle_error(e),
}
Don't ignore platform limits
// BAD: May fail on some platforms
entry.set_password(&"x".repeat(10000))?;
// GOOD: Handle TooLong errors
match entry.set_password(&long_value) {
Err(keyring::Error::TooLong(attr, limit)) => {
eprintln!("{} exceeds {} byte limit", attr, limit);
}
// ...
}
Don't access from multiple threads simultaneously
The underlying stores may not serialize concurrent access:
// BAD: Race condition on Windows/Linux
threads.iter().for_each(|_| {
entry.get_password(); // May fail randomly
});
// GOOD: Serialize access or use per-thread entries
let mutex = Mutex::new(entry);
Thread Safety
EntryisSend + Sync- However, underlying stores may not handle concurrent access
- Serialize access to the same credential from multiple threads
- Different credentials can be accessed concurrently
References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
Generate Component Documentation
Based on existing docs styles and specific API implementations, and referencing same name stories, generate comprehensive documentation for the new component.
Generate Component Story
Generate a comprehensive story for a new component for as example.
new-component
How to write a new component of GPUI Component.
troubleshooting
Diagnose and fix common Script Kit issues. Use when the user reports bugs, crashes, missing features, or unexpected behavior in Script Kit GPUI.
script-authoring
Create and manage TypeScript scripts for Script Kit. Use when the user wants to write a new script, edit an existing script, or understand Script Kit's SDK and metadata system.
agents
Create mdflow-backed agent files for Script Kit. Use when the user wants to create AI agents, configure agent backends (Claude, Gemini, Codex), or manage agent metadata.
Didn't find tool you were looking for?