A typed Python API client for pass (the standard Unix password manager)
Python 3.10+ Async Support pass-otpFull type annotations and dataclass models
Sync and async clients with concurrent operations
Complete pass-otp support for 2FA
Custom exception hierarchy
# Basic install
pip install -e .
# With dev dependencies
pip install -e ".[dev]"
from pass_client import PassClient
client = PassClient()
# Get a password
entry = client.get("websites/github")
print(entry.password)
print(entry.username) # Parsed from metadata
# Generate a new password
generated = client.generate("websites/newsite", length=20)
print(generated.password)
# Insert with metadata
client.insert("websites/example", "mypassword", metadata={
"username": "user@example.com",
"url": "https://example.com",
})
from pass_client import PassClient
client = PassClient()
# Or with custom store path
client = PassClient(store_path=Path("/custom/store"))
| Method | Description |
|---|---|
get(path) |
Get password entry with metadata |
get_password(path) |
Get just the password string |
insert(path, password, metadata) |
Insert new entry |
generate(path, length, no_symbols) |
Generate new password |
remove(path, recursive, force) |
Remove entry or directory |
move(src, dst) |
Move/rename entry |
copy(src, dst) |
Copy entry |
exists(path) |
Check if entry exists |
# Get password entry (includes metadata parsing)
entry = client.get("path/to/entry")
print(entry.password) # First line
print(entry.username) # From "username:" field
print(entry.url) # From "url:" field
print(entry.metadata) # All parsed key-value pairs
# Insert with metadata
client.insert("path/to/new", "password123", metadata={
"username": "user@example.com",
"url": "https://example.com",
})
# Generate password
generated = client.generate("path/to/entry", length=25)
print(generated.password)
print(generated.strength) # "weak", "moderate", "strong", "very_strong"
# List entries
entries = client.list() # Root level
entries = client.list("websites") # Subdirectory
for entry in entries:
print(entry.name, entry.path, entry.entry_type)
# Find by name pattern
results = client.find("github")
# Search content (grep)
results = client.grep("api.*key")
# Run git commands
output = client.git("status")
output = client.git("log", "--oneline", "-5")
# Convenience methods
client.git_push()
client.git_pull()
# Get commit history
commits = client.git_log(limit=10)
for commit in commits:
print(commit.hash, commit.message)
Async version with identical API plus concurrent operations.
import asyncio
from pass_client import AsyncPassClient
async def main():
client = AsyncPassClient()
# All methods are async
entry = await client.get("websites/github")
print(entry.password)
# Concurrent fetches
entries = await client.get_many([
"websites/github",
"websites/gitlab",
"email/personal",
])
for path, entry in entries.items():
print(f"{path}: {entry.username}")
# Concurrent inserts
results = await client.insert_many([
("sites/new1", "pass1", {"username": "user1"}),
("sites/new2", "pass2", {"username": "user2"}),
])
asyncio.run(main())
from pass_client import AsyncPassClientContext
async with AsyncPassClientContext() as client:
entry = await client.get("websites/github")
Full support for the pass-otp extension for two-factor authentication.
apt install pass-extension-otp (Debian/Ubuntu) or brew install pass-otp (macOS)
| Method | Description |
|---|---|
otp_code(path) |
Generate current OTP code |
otp_code_clip(path) |
Copy OTP to clipboard |
otp_uri(path) |
Get parsed OTP URI |
otp_insert(path, uri) |
Insert new OTP entry |
otp_append(path, uri) |
Add OTP to existing entry |
otp_validate(uri) |
Validate URI syntax |
otp_get(path) |
Get full OTPEntry |
otp_has(path) |
Check if OTP configured |
otp_code_many(paths) |
Concurrent codes (async) |
from pass_client import PassClient, OTPUri
client = PassClient()
# Generate OTP code
code = client.otp_code("2fa/github")
print(code.code) # "123456"
print(code.formatted) # "123 456"
# Get OTP URI details
uri = client.otp_uri("2fa/github")
print(uri.issuer) # "GitHub"
print(uri.account) # "user@example.com"
print(uri.secret) # Base32 secret
print(uri.algorithm) # OTPAlgorithm.SHA1
print(uri.digits) # 6
print(uri.period) # 30 (TOTP)
# Insert OTP (new entry)
client.otp_insert("2fa/gitlab",
"otpauth://totp/GitLab:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitLab")
# Insert with just the secret
client.otp_insert("2fa/aws", "ABCDEFGHIJKLMNOP", from_secret=True)
# Append OTP to existing password entry
client.otp_append("websites/github",
"otpauth://totp/GitHub:user?secret=SECRETKEY&issuer=GitHub")
# Check if OTP is configured
if client.otp_has("websites/github"):
code = client.otp_code("websites/github")
from pass_client import OTPUri, OTPType, OTPAlgorithm
# Parse from string
uri = OTPUri.from_uri(
"otpauth://totp/GitHub:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"
)
print(uri.otp_type) # OTPType.TOTP
print(uri.issuer) # "GitHub"
print(uri.account) # "user@example.com"
print(uri.secret) # "JBSWY3DPEHPK3PXP"
# Convert back to URI string
uri_string = uri.to_uri()
async def get_all_codes():
client = AsyncPassClient()
# Fetch multiple OTP codes concurrently
codes = await client.otp_code_many([
"2fa/github",
"2fa/gitlab",
"2fa/aws",
])
for path, code in codes.items():
print(f"{path}: {code.formatted}")
All exceptions inherit from PassError:
from pass_client import (
PassClient,
PassError,
PassNotFoundError,
PassGPGError,
PassOTPNotFoundError,
PassOTPExtensionNotFoundError,
)
client = PassClient()
try:
entry = client.get("nonexistent/path")
except PassNotFoundError as e:
print(f"Entry not found: {e.path}")
except PassGPGError as e:
print(f"GPG decryption failed: {e.message}")
except PassError as e:
print(f"Pass error: {e.message}")
PassError
├── PassNotFoundError
├── PassStoreNotInitializedError
├── PassGPGError
├── PassClipboardError
├── PassGitError
├── PassGenerateError
├── PassInsertError
└── PassOTPError
├── PassOTPNotFoundError
├── PassOTPInvalidURIError
└── PassOTPExtensionNotFoundError
@dataclass
class PasswordEntry:
path: str
password: str
metadata: dict[str, str]
# Properties
username: Optional[str] # From "username:" or "user:"
url: Optional[str] # From "url:" or "site:"
notes: Optional[str] # From "notes:"
@dataclass
class OTPCode:
code: str
path: str
remaining_seconds: Optional[int]
# Properties
formatted: str # "123 456" or "1234 5678"
@dataclass
class OTPUri:
uri: str
otp_type: OTPType # TOTP or HOTP
issuer: Optional[str]
account: str
secret: str
algorithm: OTPAlgorithm # SHA1, SHA256, SHA512
digits: int # 6 or 8
period: int # TOTP interval (default 30)
counter: Optional[int] # HOTP counter
| Model | Description |
|---|---|
StoreEntry |
Entry in pass tree (name, path, type) |
GeneratedPassword |
Generated password with strength rating |
StoreInfo |
Password store metadata |
GitCommit |
Git commit info (hash, message, author, timestamp) |
SearchResult |
Find/grep result with context |
OTPEntry |
Full OTP entry (path + OTPUri) |
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run tests with coverage
pytest --cov=pass_client
# Type checking
mypy pass_client
# Linting
ruff check pass_client