This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
cotp is a command-line TOTP/HOTP authenticator written in Rust (edition 2024). It stores all secrets in a single encrypted database file and provides both a subcommand CLI and an interactive terminal dashboard (TUI). The crate forbids unsafe_code (#![forbid(unsafe_code)] in main.rs).
cargo build # debug build (version suffixed -DEBUG-<commit>)
cargo build --release # optimized release build
cargo test --locked # run all tests (unit + integration)
cargo test --locked --release # tests in release mode (CI runs both)
cargo test <name> # run a single test by substring match
cargo fmt --all -- --check # formatting check (CI gate)
cargo clippy -- -D warnings # lint; warnings are errors (CI gate)CI (.github/workflows/build.yml) requires cargo fmt, cargo clippy -D warnings, and cargo test (debug + release) to pass across Linux/macOS/Windows. On Linux, building requires xcb dev libraries for clipboard support (see README "Other linux distributions").
Before finishing any work you MUST run clippy exactly as CI does — cargo clippy -- -D warnings — and fix every issue it reports. CI treats warnings as errors, so any lint left unaddressed will fail the pipeline. Do not consider a task complete until this command exits cleanly.
The Python converters have their own tests: cd converters && python -m pytest (or run test_converters.py), exercised against fixtures in example_databases/.
Commits must follow the Conventional Commits / Angular convention: a type(scope): subject header where type is one of feat, fix, docs, style, refactor, perf, test, build, ci, chore, or revert. The scope is optional (e.g. feat(import): ...).
Releases are fully automated by semantic-release on every push to main (.releaserc, .github/workflows/release.yml), and the commit types are what drive the version bump — there is no manual editing of the version in Cargo.toml:
fix:→ patch release (e.g. 1.9.10 → 1.9.11)feat:→ minor release (e.g. 1.9.10 → 1.10.0)- a commit with a
BREAKING CHANGE:footer (or!after the type, e.g.feat!:) → major release - other types (
docs,chore,ci, …) → no release
commit-analyzer computes the next version, ci/write_cargo_version.sh writes it into Cargo.toml/Cargo.lock, CHANGELOG.md is regenerated, and the Deploy workflow is triggered. Choose the commit type deliberately — it directly determines the published version. Keep one logical change per commit so the release notes stay meaningful.
main.rs drives a fixed lifecycle for every invocation:
init()— resolves the DB path, and either initializes a new encrypted database on first run (prompting for a password) or loads the existing one. ReturnsReadResult = (OTPDatabase, key, salt).args_parser()(arguments/mod.rs) — if a subcommand was given, dispatches to it; otherwise launches the interactivedashboard()(TUI). Each subcommand consumes theOTPDatabaseand returns a (possibly modified) one.- Back in
main(), ifdatabase.is_modified()the database is re-encrypted and written to disk. The derivedkeyis zeroized before exit.
The OTPDatabase is passed by value through the command layer; mutations set a needs_modification flag (via mark_modified()) that gates the final save. Secrets and keys use zeroize throughout — preserve zeroization when touching password/key handling.
arguments/— Clap subcommands (add,edit,list,delete,import,export,extract,passwd). Each implements theSubcommandExecutortrait (fn run_command(self, db: OTPDatabase) -> Result<OTPDatabase>), wired together withenum_dispatchon theCotpSubcommandsenum. To add a subcommand: create the module, define anArgsstruct, implementSubcommandExecutor, and add a variant toCotpSubcommands.otp/— core domain.otp_element.rsholdsOTPElementandOTPDatabase(serialization, save/encrypt, migrations).algorithms/has one generator per scheme (totp,hotp,motp,steam,yandex).otp_type.rs/otp_algorithm.rsare the enums;from_otp_uri.rsparsesotpauth://URIs.crypto/—cryptography.rsdoes Argon2id key derivation (config constants at top of file) + XChaCha20Poly1305 authenticated encryption; also AES-GCM for decrypting Aegis encrypted backups.encrypted_database.rsis the on-disk envelope.importers/— one module per source app.importer.rs::import_from_path::<T>()is the generic entry point:Tmust beDeserialize + TryInto<Vec<OTPElement>>. Import selection happens inarguments/import.rs. Some sources (Authy, Microsoft Authenticator, FreeOTP) are pre-converted by Python scripts toConvertedJsonListfirst (see below); others deserialize natively. Google Authenticator is handled natively bygoogle_authenticator.rs, which parsesotpauth-migration://export URIs (base64 protobufMigrationPayload, decoded withprostusing hand-declared message structs — no.proto/protocbuild step).exporters/—andotp,freeotp_plus,otp_uri.do_export::<T: Serialize>()is the shared writer.interface/— the ratatui/crossterm TUI.app.rsholds mutableAppstate;ui.rsrenders;event.rsis the input event loop (250ms tick);handlers/route key events by focus (main_window,popup,search_bar). The dashboard runs onio::stderr()so stdout stays clean for piping.
- Default path resolution (
path.rs):--database-patharg >COTP_DB_PATHenv >./db.cotp(portable / debug builds always) >$XDG_DATA_HOME/cotp/db.cotp(auto-migrated from legacy$HOME/.cotp/db.cotpif present). The path is aOnceLockset once at startup. CURRENT_DATABASE_VERSION(inotp_element.rs) is the schema version. Legacy v1 was a bareVec<OTPElement>;read_from_filefalls back to parsing that and converts viaFrom<Vec<OTPElement>>. Schema upgrades go inotp/migrations/mod.rs— add aMigration { to_version, migration_function }entry toMIGRATIONS_LIST;migrate()runs on every save.
Some app backups are binary/XML and need pre-conversion into cotp's JSON (ConvertedJsonList) before cotp import. Scripts: authy.py, gauth.py, mauth.py, freeotp.py. Usage: python <script>.py path/to/backup converted.json, then cotp import --<app> --path converted.json. Fixtures live in example_databases/; keep test_converters.py in sync when changing output shape.
tests/holds integration tests usingassert_cmd/assert_fs/predicatesthat invoke the builtcotpbinary with a throwaway database.test_samples/holds import/export fixtures.- The password from stdin (
--password-stdin) and--database-pathflags are the primary levers for scripting cotp non-interactively in tests.