kcpwd
Hi everyone
In local development, it’s common to store credentials in .env files or even hardcode them temporarily during testing 😬.
While tools like Vault handle secrets perfectly in production, setting them up for small local projects can sometimes feel like overkill.
That’s why I built kcpwd — a tiny, native Python CLI and decorator-based tool to handle secrets without needing Vault.
https://github.com/osmanuygar/kcpwd
Why I built it
During local development, I often found myself doing one of the following:
- Storing secrets in
.envfiles (and hoping they don’t leak 😅) - Hardcoding temporary credentials into test code
- Avoiding Vault setup because it felt too heavy for a small project
So, I wanted something lightweight that would let me store and access secrets securely, using just Python and a simple CLI.
That’s how kcpwd was born.
No need to expose secrets in your code — kcpwd will fetch them securely at runtime.
Why not just use Vault?
Vault is amazing, but for local-only or experimental projects, it can be too complex to set up.
kcpwd is designed as a mini Vault alternative for local use, offering:
- No dependencies beyond Python
- No separate service or container to run
- Simple, fast, and native credential handling
Features
- Python-native
- Decorator + CLI interface
- No external dependencies
- Works locally or inside containers
- Lightweight and developer-friendly
- Secure storage using macOS Keychain
- Python library for programmatic access
- Native macOS integration
Example use cases
- Local development environments
- Internal scripts or prototypes
- Testing Dockerized apps without exposing secrets
- CI/CD debugging or credential injection during testing
Installation
From PyPI
pip install kcpwd
From Source
git clone https://github.com/osmanuygar/kcpwd.git
cd kcpwd
pip install -e .
Usage
CLI Usage
Store a password
kcpwd set dbadmin asd123
Retrieve a password (copies to clipboard)
kcpwd get dbadmin
Delete a password
kcpwd delete dbadmin
List all stored passwords
kcpwd list
Note: The list command uses security dump-keychain which may take a few seconds and requires access to your keychain. If it shows "No passwords stored" but you know you have passwords, try using Keychain Access app to verify.
Generate a secure password
# Generate a 16-character password (default)
kcpwd generate
# Generate a 20-character password
kcpwd generate -l 20
# Generate without symbols (alphanumeric only)
kcpwd generate --no-symbols
# Generate and save immediately
kcpwd generate -s myapi
# Generate a 6-digit PIN
kcpwd generate -l 6 --no-uppercase --no-lowercase --no-symbols
# Generate without ambiguous characters (no 0/O, 1/l/I)
kcpwd generate --exclude-ambiguous
Export passwords (NEW!)
# Export all passwords to a JSON file
kcpwd export backup.json
# Export only key names (without passwords)
kcpwd export keys.json --keys-only
# Force overwrite existing file
kcpwd export backup.json -f
** Security Warning*: Exported files contain passwords in **PLAIN TEXT*. Keep them secure!
Import passwords (NEW!)
# Import passwords (skip existing keys)
kcpwd import backup.json
# Import and overwrite existing passwords
kcpwd import backup.json --overwrite
# Preview what would be imported without making changes
kcpwd import backup.json --dry-run
Library Usage
Basic Functions
from kcpwd import set_password, get_password, delete_password
# Store a password
set_password("my_database", "secret123")
# Retrieve a password
password = get_password("my_database")
print(password) # Output: secret123
# Retrieve and copy to clipboard
password = get_password("my_database", copy_to_clip=True)
# Delete a password
delete_password("my_database")
Password Generation
from kcpwd import generate_password
# Generate a secure password
password = generate_password(length=20)
print(password) # Output: 'aB3#xK9!mL2$nP5@qR7&'
# Generate alphanumeric password (no symbols)
password = generate_password(length=16, use_symbols=False)
print(password) # Output: 'aB3xK9mL2nP5qR7t'
# Generate a 6-digit PIN
pin = generate_password(
length=6,
use_uppercase=False,
use_lowercase=False,
use_symbols=False
)
print(pin) # Output: '384729'
List All Keys (NEW!)
from kcpwd import list_all_keys
# Get all stored password keys
keys = list_all_keys()
print(keys) # Output: ['my_database', 'api_key', 'email_password']
# Check if a specific key exists
if 'my_database' in list_all_keys():
print("Database password exists!")
Export/Import (NEW!)
from kcpwd import export_passwords, import_passwords
# Export all passwords
result = export_passwords('backup.json')
print(f"Exported {result['exported_count']} passwords")
# Export only keys (without passwords)
result = export_passwords('keys_only.json', include_passwords=False)
# Import passwords (skip existing)
result = import_passwords('backup.json')
print(f"Imported {result['imported_count']} passwords")
print(f"Skipped {len(result['skipped_keys'])} existing keys")
# Import with overwrite
result = import_passwords('backup.json', overwrite=True)
# Dry run to preview import
result = import_passwords('backup.json', dry_run=True)
print(result['message'])
Using Decorators (Recommended!)
The @require_password decorator automatically injects passwords from keychain:
from kcpwd import require_password, set_password
# First, store your password
set_password("my_db", "secret123")
# Use the decorator to auto-inject password
@require_password('my_db')
def connect_to_database(host, username, password=None):
print(f"Connecting to {host} as {username}")
print(f"Password: {password}")
# Your database connection code here
return f"Connected with password: {password}"
# Call without password - it's automatically retrieved!
result = connect_to_database("localhost", "admin")
# Output: Connected with password: secret123
Advanced Decorator Usage
You can specify different parameter names:
from kcpwd import require_password, set_password
# Store API key
set_password("github_api", "ghp_xxxxxxxxxxxx")
# Inject into custom parameter name
@require_password('github_api', param_name='api_key')
def call_github_api(endpoint, api_key=None):
print(f"Calling GitHub API: {endpoint}")
print(f"Using key: {api_key}")
# Your API call code here
return {"status": "success"}
# API key automatically retrieved from keychain
response = call_github_api("/user/repos")
Real-World Examples
Database Connection:
#import psycopg2
from kcpwd import require_password, set_password
# Setup: Store password once
set_password("prod_db", "my_secure_password")
# Use in your code
@require_password('prod_db')
def get_db_connection(host, user, database, password=None):
return psycopg2.connect(
host=host,
user=user,
password=password,
database=database
)
# No need to handle password manually!
conn = get_db_connection(
host="prod.example.com",
user="dbuser",
database="myapp"
)
Backup Script:
from kcpwd import export_passwords, import_passwords
import os
from datetime import datetime
def backup_passwords():
"""Create a timestamped backup of all passwords"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_file = f'passwords_backup_{timestamp}.json'
result = export_passwords(backup_file)
if result['success']:
print(f"✓ Backup created: {backup_file}")
print(f" Exported {result['exported_count']} passwords")
# Move to secure location
secure_dir = os.path.expanduser('~/Documents/Backups')
os.makedirs(secure_dir, exist_ok=True)
os.rename(backup_file, os.path.join(secure_dir, backup_file))
else:
print(f"✗ Backup failed: {result['message']}")
def restore_passwords(backup_file):
"""Restore passwords from a backup"""
# Preview first
result = import_passwords(backup_file, dry_run=True)
print(f"Preview: {result['message']}")
# Confirm
if input("Continue with import? (y/n): ").lower() == 'y':
result = import_passwords(backup_file, overwrite=True)
print(f"✓ {result['message']}")
else:
print("Import cancelled")
Migration Script:
from kcpwd import export_passwords, import_passwords, list_all_keys
def migrate_to_new_machine(export_path='~/migration.json'):
"""Export passwords for migration to a new machine"""
export_path = os.path.expanduser(export_path)
result = export_passwords(export_path)
if result['success']:
print(f"✓ Exported {result['exported_count']} passwords")
print(f" File: {export_path}")
print("\nOn your new machine, run:")
print(f" kcpwd import {export_path}")
else:
print(f"✗ Export failed: {result['message']}")
def complete_migration(import_path='~/migration.json'):
"""Complete migration on new machine"""
import_path = os.path.expanduser(import_path)
# Check existing passwords
existing = list_all_keys()
if existing:
print(f"Found {len(existing)} existing passwords")
print("Use --overwrite to replace them")
# Import
result = import_passwords(import_path)
if result['success']:
print(f"✓ {result['message']}")
# Clean up migration file
if input("Delete migration file? (y/n): ").lower() == 'y':
os.remove(import_path)
print("✓ Migration file deleted")
else:
print(f"✗ Import failed: {result['message']}")
Export File Format
The export JSON file has the following structure:
{
"exported_at": "2025-01-15T10:30:00.123456",
"service": "kcpwd",
"version": "0.3.0",
"include_passwords": true,
"passwords": [
{
"key": "my_database",
"password": "secret123"
},
{
"key": "api_key",
"password": "sk-xxxxxxxxxxxxx"
}
]
}
How It Works
kcpwd stores your passwords in the macOS Keychain - the same secure, encrypted storage that Safari and other macOS apps use. This means:
- Passwords are encrypted with your Mac's security
- They persist across reboots
- They're protected by your Mac's login password
- No plain text files or databases
- Can be accessed programmatically via Python
Viewing Your Passwords
Open Keychain Access app and search for "kcpwd" to see all stored passwords.
Or use Terminal:
security find-generic-password -s "kcpwd" -a "dbadmin" -w
💬 Contribute or share feedback
The project is open-source and actively evolving.
If you’d like to try it, give feedback, or open an issue:
GitHub: https://github.com/osmanuygar/kcpwd
Thanks for reading! If you also love building small but useful dev tools, feel free to share your experience in the comments 🚀
Tags:
#python #security #cli #opensource #devtools
Top comments (0)