pass-client

A typed Python API client for pass (the standard Unix password manager)

Python 3.10+ Async Support pass-otp

Features

Type Hints

Full type annotations and dataclass models

Async Support

Sync and async clients with concurrent operations

OTP Extension

Complete pass-otp support for 2FA

Error Handling

Custom exception hierarchy

Installation

Requirements

Install from source

# Basic install
pip install -e .

# With dev dependencies
pip install -e ".[dev]"

Quick Start

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",
})

PassClient (Sync)

Initialization

from pass_client import PassClient

client = PassClient()
# Or with custom store path
client = PassClient(store_path=Path("/custom/store"))

Password Operations

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

Examples

# 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"

Search Operations

# 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")

Git Operations

# 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)

AsyncPassClient

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())

Context Manager

from pass_client import AsyncPassClientContext

async with AsyncPassClientContext() as client:
    entry = await client.get("websites/github")

OTP Support (pass-otp)

Full support for the pass-otp extension for two-factor authentication.

Install pass-otp: apt install pass-extension-otp (Debian/Ubuntu) or brew install pass-otp (macOS)

OTP Methods

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)

Usage Examples

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")

Parsing OTP URIs

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 Concurrent OTP

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}")

Error Handling

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}")

Exception Hierarchy

PassError
├── PassNotFoundError
├── PassStoreNotInitializedError
├── PassGPGError
├── PassClipboardError
├── PassGitError
├── PassGenerateError
├── PassInsertError
└── PassOTPError
    ├── PassOTPNotFoundError
    ├── PassOTPInvalidURIError
    └── PassOTPExtensionNotFoundError

Models

PasswordEntry

@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:"

OTPCode

@dataclass
class OTPCode:
    code: str
    path: str
    remaining_seconds: Optional[int]

    # Properties
    formatted: str  # "123 456" or "1234 5678"

OTPUri

@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

Other Models

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)

Development

# 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