Programmatic Usage
While PocketProtector’s CLI is its primary interface, the Python API enables direct integration into applications and scripts. This guide covers real-world usage patterns.
Basic decryption
The most common pattern: load a protected file, authenticate, and decrypt a domain.
from pocket_protector import KeyFile, Creds
kf = KeyFile.from_file('protected.yaml')
creds = Creds(name='alice@example.com', passphrase='my-passphrase')
secrets = kf.decrypt_domain('prod', creds)
# secrets is a dict-like object
db_password = secrets['db-password']
api_key = secrets['api-key']
The returned object raises PPError (specifically
PPKeyError) if you access a secret that doesn’t exist, with a
helpful error message listing known secrets.
CRUD operations
All mutating operations return a new KeyFile instance (the class is
immutable). You must call .write() to persist changes.
from pocket_protector import KeyFile, Creds
kf = KeyFile.from_file('protected.yaml')
# Add a secret (fails if it already exists)
kf = kf.add_secret('dev', 'new-api-key', 'secret-value')
# Update a secret (fails if it doesn't exist)
kf = kf.update_secret('dev', 'new-api-key', 'updated-value')
# Set a secret (add or update, no error either way)
kf = kf.set_secret('dev', 'new-api-key', 'another-value')
# Remove a secret
kf = kf.rm_secret('dev', 'new-api-key')
# Persist all changes
kf.write()
Important
Each method returns a new KeyFile. Forgetting to capture the
return value means your changes are lost:
# WRONG: result is discarded
kf.add_secret('dev', 'key', 'value')
# RIGHT: capture the new instance
kf = kf.add_secret('dev', 'key', 'value')
Bootstrapping a new protected
Creating a protected file from scratch, programmatically:
from pocket_protector import KeyFile, Creds, KDF_INTERACTIVE
# Create the file
kf = KeyFile.create('protected.yaml')
# Add a key custodian
creds = Creds(name='admin@example.com', passphrase='admin-pass')
kf = kf.add_key_custodian(creds, *KDF_INTERACTIVE)
# Create a domain with the custodian as owner
kf = kf.add_domain('dev', 'admin@example.com')
# Add secrets
kf = kf.add_secret('dev', 'db-password', 'p4ssw0rd')
kf = kf.add_secret('dev', 'api-key', 'ak_12345')
# Write to disk
kf.write()
Introspection
Inspect a protected file without decrypting anything:
kf = KeyFile.from_file('protected.yaml')
# List domains
kf.get_domain_names()
# ['dev', 'prod']
# List secrets in a domain
kf.get_domain_secret_names('dev')
# ['api-key', 'db-password']
# Map of secret names to domains that contain them
kf.get_all_secret_names()
# {'api-key': ['dev', 'prod'], 'db-password': ['dev']}
# Audit log
kf.get_audit_log()
# ['2026-01-01T00:00:00Z -- created key custodian admin@example.com', ...]
# Domains a custodian owns
kf.get_custodian_domains('admin@example.com')
# ['dev', 'prod']
Key management
from pocket_protector import KeyFile, Creds, KDF_INTERACTIVE
kf = KeyFile.from_file('protected.yaml')
# Add a new custodian
new_creds = Creds(name='bob@example.com', passphrase='bob-pass')
kf = kf.add_key_custodian(new_creds, *KDF_INTERACTIVE)
# Grant domain access (requires existing owner credentials)
admin_creds = Creds(name='admin@example.com', passphrase='admin-pass')
kf = kf.add_owner('dev', 'bob@example.com', admin_creds)
# Migrate all ownerships to another custodian
kf = kf.migrate_owner('bob@example.com', admin_creds)
# Rotate domain keys (after removing an owner)
kf = kf.rm_owner('dev', 'old-user@example.com')
kf = kf.rotate_domain_key('dev', admin_creds)
kf.write()
Config integration pattern
A pattern used in real-world applications: TOML or INI config values
prefixed with protected: are resolved from a decrypted domain.
import tomllib
from pocket_protector import KeyFile, Creds
PROTECTED_PREFIX = 'protected:'
def load_config(config_path, protected_path, domain, creds):
"""Load config, resolving protected: prefixed values from a domain."""
with open(config_path, 'rb') as f:
config = tomllib.load(f)
kf = KeyFile.from_file(protected_path)
secrets = kf.decrypt_domain(domain, creds)
def resolve(obj):
if isinstance(obj, str) and obj.startswith(PROTECTED_PREFIX):
secret_name = obj[len(PROTECTED_PREFIX):]
return secrets[secret_name]
if isinstance(obj, dict):
return {k: resolve(v) for k, v in obj.items()}
return obj
return resolve(config)
Example config (config.toml):
[database]
host = "db.example.com"
password = "protected:db-password"
[email]
api_key = "protected:mail-api-key"
Subprocess integration
When PocketProtector is used from non-Python applications, or when
process isolation is desired, shell out to pprotect:
import subprocess
import json
import os
def decrypt_via_cli(domain, user, passphrase):
"""Decrypt a domain using the pprotect CLI."""
env = {
'PATH': os.environ['PATH'],
'PPROTECT_USER': user,
'PPROTECT_PASSPHRASE': passphrase,
}
result = subprocess.run(
['pprotect', 'decrypt-domain', '--non-interactive', domain],
capture_output=True, text=True, env=env,
)
result.check_returncode()
return json.loads(result.stdout)
When to use the CLI vs. the API:
Use the API when your application is Python and you want to avoid subprocess overhead, or when you need fine-grained control (CRUD operations, key management).
Use the CLI when calling from a non-Python language, when you want process isolation (
exec), or when working in shell scripts.
Passphrase management
In automation, passphrases typically come from environment variables.
The Creds.from_env() classmethod handles this:
from pocket_protector import Creds
# Reads PPROTECT_USER and PPROTECT_PASSPHRASE
creds = Creds.from_env()
# Or with a custom prefix (reads MYAPP_USER and MYAPP_PASSPHRASE)
creds = Creds.from_env(prefix='MYAPP')
When prefix is not passed, from_env() checks the
PPROTECT_ENV_PREFIX environment variable before falling back to
PPROTECT.
The resulting Creds object has name_source and
passphrase_source set automatically, so error messages will
reference the correct env var names.