DEV Community

osman uygar köse
osman uygar köse

Posted on

kcpwd: A Minimal Python CLI to Keep Your Secrets Safe in Local Development

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 .env files (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
Enter fullscreen mode Exit fullscreen mode

From Source

git clone https://github.com/osmanuygar/kcpwd.git
cd kcpwd
pip install -e .
Enter fullscreen mode Exit fullscreen mode

Usage

CLI Usage

Store a password

kcpwd set dbadmin asd123
Enter fullscreen mode Exit fullscreen mode

Retrieve a password (copies to clipboard)

kcpwd get dbadmin
Enter fullscreen mode Exit fullscreen mode

Delete a password

kcpwd delete dbadmin
Enter fullscreen mode Exit fullscreen mode

List all stored passwords

kcpwd list
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

** 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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

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'])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"
)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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']}")
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💬 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


Enter fullscreen mode Exit fullscreen mode

Top comments (0)