tofa

TOFA TUI showing 21 accounts with live OTP codes   TOFA macOS menu bar app showing the same accounts

tofa is an offline, encrypted 2FA tool for the terminal. Secrets stay on your machine in an AES-256-GCM vault — no cloud, no account, no telemetry.

Looking for the marketing site, installer one-liner, demos, and FAQ? → https://tofa.stratif.io

This site primarily documents the command-line interface and its companion TUI. The macOS menu bar app — pictured above — reads the same vault and is covered in the project README.

What's in here

  • Getting started — install, create a vault, add your first account.
  • CLI reference — every subcommand, with flags auto-synced to the source.
  • Recipes — common workflows like importing from Aegis or scripting with TOFA_PASSPHRASE.
  • Security model — what the vault protects, and how.

Installation

Shell installer (macOS or Linux)

The quickest way — no Rust required:

curl -fsSL https://tofa.stratif.io/install.sh | sh

Installs to ~/.local/bin/tofa. To pin a specific version:

VERSION=0.11.0 curl -fsSL https://tofa.stratif.io/install.sh | sh

Homebrew (macOS or Linux)

brew tap stratif-io/tofa
brew install tofa

Cargo (from crates.io)

cargo install tofa

From source

tofa is written in Rust. Install rustup (1.78 or newer), then:

git clone https://github.com/stratif-io/tofa
cd tofa
cargo install --path tofa

The binary tofa lands in ~/.cargo/bin.

Verify the install

tofa --version

What's next

Create your first vault: see Quick start.

Quick start

1. Create a vault

tofa init

You'll be asked for a passphrase. The vault is created at ~/Library/Application Support/tofa/vault.enc on macOS or ~/.config/tofa/vault.enc on Linux.

2. Add an account

From a base32 secret:

tofa add --name "GitHub:you" --secret JBSWY3DPEHPK3PXP

From an otpauth:// URI:

tofa add --uri "otpauth://totp/GitHub:you?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"

From a QR image on disk:

tofa add --qr ~/Downloads/github-qr.png

3. Get a code

tofa code GitHub:you

Pipe to your clipboard:

tofa code GitHub:you | pbcopy   # macOS
tofa code GitHub:you | xclip    # Linux

4. Open the TUI

tofa

Live countdown bars, mouse support, violet accent.

Vault & passphrase

The vault is a single AES-256-GCM-encrypted file containing every account's secret. The passphrase you set during tofa init is the only key — losing it makes the vault unrecoverable.

Where it lives

OSDefault path
macOS~/Library/Application Support/tofa/vault.enc
Linux~/.config/tofa/vault.enc

Override with the --vault <PATH> flag (works on every command) or the TOFA_VAULT env var.

Passphrase cache

Once you unlock the vault in the TUI or app, the passphrase is held in memory for 10 minutes of inactivity, then zeroed. Each CLI invocation prompts fresh — there is no daemon.

Changing the passphrase

tofa rekey

You'll be asked for the current and new passphrases. The vault is rewritten atomically.

Destroying the vault

tofa destroy

Permanently deletes the vault file. There is no undo.

See also

CLI reference

Every tofa subcommand documented in one place. Auto-generated synopsis and flags are kept in sync with the source by an xtask tool — they cannot drift.

Global options

These work on every subcommand:

FlagDescription
--vault <PATH>Override the vault path. Reads from TOFA_VAULT env var as fallback.
--help, -hPrint help.
--version, -VPrint the version.

Environment variables

VariableEffect
TOFA_VAULTDefault vault path when --vault is not given.
TOFA_PASSPHRASEIf set, used instead of an interactive prompt. Avoid in productiontofa prints a warning to stderr when it reads this.

Commands

CommandPurpose
initCreate a new encrypted vault.
listList every account.
codePrint the current TOTP code for one account.
addAdd an account from secret, URI, or QR.
removeRemove an account.
renameRename an account.
qrPrint a QR code for one account or all.
rekeyChange the vault passphrase.
exportDump every account as JSON or as a list of otpauth:// URIs.
importImport from any supported file: single- or multi-QR images, migration QRs, JSON / CSV / TXT exports from other authenticators, or a zip archive of any of the above.
scanCapture every connected display and import every QR visible on screen.
camOpen the webcam and wait for a QR.
completionsPrint shell completions.
destroyPermanently delete the vault.

tofa init

Create a new encrypted vault. Run this once before any other command.

Synopsis

tofa init

Examples

Initialize the default vault:

$ tofa init
Choose a passphrase: ********
Confirm passphrase: ********
✓ vault created at /Users/you/Library/Application Support/tofa/vault.enc

Use a custom path:

$ tofa --vault ~/secrets/work-vault.enc init

Or via env var:

$ TOFA_VAULT=~/secrets/work-vault.enc tofa init

Notes

  • Exit code 0 on success.
  • Refuses to overwrite an existing vault file. Move or delete it first if you really want to start over (consider destroy for that).
  • The passphrase is asked twice and must match. There is no recovery — losing the passphrase makes the vault unreadable forever.

See also

tofa list

List every account in the vault, sorted by id. Pass --codes to also print the current TOTP and time remaining.

Synopsis

tofa list [FLAGS]

Flags

FlagDescription
--codes <CODES>Also display current codes and time remaining

Examples

Names only (fastest):

$ tofa list
Passphrase: ********
GitHub:you
Discord:you
Slack:work

With live codes and countdown:

$ tofa list --codes
Passphrase: ********
┌─────────────────────────────────────┬──────────┬─────────┐
│ name                                │ code     │ expires │
├─────────────────────────────────────┼──────────┼─────────┤
│ GitHub:you                          │ 482 913  │ 21s     │
│ Discord:you                         │ 705 224  │ 21s     │
│ Slack:work                          │ 169 380  │ 21s     │
└─────────────────────────────────────┴──────────┴─────────┘

Notes

  • Output is sorted alphabetically by id.
  • --codes is a switch (no value); the <CODES> placeholder in the auto-generated table above is a clap quirk for boolean flags. Just write --codes.
  • For a single account in a script, prefer tofa code.

See also

tofa code

Print the current TOTP code for one account. Takes the account id (or a prefix) as the first positional argument.

Synopsis

tofa code [FLAGS]

Flags

FlagDescription
--raw <RAW>Output bare digits without space (for scripting)
--copy <COPY>Copy to clipboard (the code by default; the otpauth:// URI when --uri is set)
--watch <WATCH>Refresh every second until Ctrl+C
--uri <URI>Print/copy the entry's otpauth:// URI instead of the current code. Useful for moving an account to another authenticator app or piping into tofa add --uri

Examples

Print the code (with the conventional space):

$ tofa code GitHub:you
Passphrase: ********
482 913   (21s left)

Bare digits for scripts:

$ tofa code GitHub:you --raw
Passphrase: ********
482913

Copy to clipboard (uses your platform's clipboard handler):

$ tofa code GitHub:you --copy
Passphrase: ********
✓ copied

Live-watch a code (handy when typing it elsewhere):

$ tofa code GitHub:you --watch
482 913   (21s left)
482 913   (20s left)
...

Pipe to your own clipboard tool if --copy doesn't fit:

tofa code GitHub:you --raw | pbcopy   # macOS
tofa code GitHub:you --raw | xclip    # Linux

Print or copy the entry's otpauth:// URI instead of the current code. Useful for moving one account to another authenticator without exporting the whole vault:

$ tofa code GitHub:you --uri
otpauth://totp/GitHub%3Ayou?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA1&digits=6&period=30

$ tofa code GitHub:you --uri --copy
otpauth://totp/GitHub%3Ayou?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA1&digits=6&period=30
✓ copied

The URI carries every parameter (period / digits / algorithm), so the receiving authenticator gets an exact copy of the entry.

Notes

  • The first argument is the account id or name — partial matches work. If a prefix is ambiguous, tofa lists the candidates and exits non-zero.
  • --raw, --copy, --watch are all switches; the <RAW> etc. placeholders in the auto-generated table above are a clap quirk for booleans.
  • Exit code 1 on missing account, wrong passphrase, or ambiguous prefix.

See also

tofa add

Add a new account to the vault. Accepts a base32 --secret, an otpauth:// URI, or a path to a QR image.

Synopsis

tofa add [FLAGS]

Flags

FlagDescription
--name <NAME>Account name (required when using --secret)
--secret <BASE32>Base32-encoded TOTP secret
--uri <URI>otpauth:// URI
--qr <PATH>Path to a QR code image

Examples

From a base32 secret:

$ tofa add --name "GitHub:you" --secret JBSWY3DPEHPK3PXP
Passphrase: ********
✓ added GitHub:you

From an otpauth:// URI (issuer and label parsed automatically):

$ tofa add --uri "otpauth://totp/GitHub:you?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"
Passphrase: ********
✓ added GitHub:you

From a QR image:

$ tofa add --qr ~/Downloads/github-qr.png
Passphrase: ********
✓ added GitHub:you (issuer=GitHub, label=you)

A migration QR (Google Authenticator export, contains many accounts) imports all of them at once:

$ tofa add --qr ~/Downloads/migration.png
Passphrase: ********
Imported 3 account(s).

If the migration QR includes accounts you've already imported, the duplicates are skipped silently and the count reflects only the new ones:

$ tofa add --qr ~/Downloads/migration.png
Passphrase: ********
Imported 1 account(s) (2 duplicate(s) skipped).

Re-adding an account that's already in the vault errors out instead of creating a duplicate row:

$ tofa add --uri "otpauth://totp/GitHub:you?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"
Passphrase: ********
Error: "GitHub:you" is already in the vault.

Notes

  • Exactly one of --secret, --uri, or --qr is required.
  • --secret requires --name; --uri and --qr derive the name themselves (override with --name).
  • An entry is a duplicate when its name and secret both match a row already in the vault. Rotating the secret (same name, new secret) or filing the same secret under a second name still goes through. This is the same rule the TUI, desktop app, and tofa import use.
  • The vault is rewritten atomically: a temp file is written and then renamed, so the old vault is never partially overwritten.
  • Exit code 0 on success, non-zero on parse errors, wrong passphrase, or an attempt to re-add an exact duplicate.

See also

  • tofa scan — capture the screen instead of supplying a file.
  • tofa cam — webcam capture.
  • tofa import — bulk import from a JSON export.

tofa remove

Remove an account from the vault. Takes the id (or a prefix) as the first positional argument and prompts for confirmation.

Synopsis

tofa remove

Examples

By exact id:

$ tofa remove GitHub:you
Passphrase: ********
Remove "GitHub:you"? [y/N] y
Removed "GitHub:you".

By prefix (works when only one entry matches):

$ tofa remove GitHub
Passphrase: ********
Remove "GitHub:you"? [y/N] y
Removed "GitHub:you".

Cancel at the prompt:

$ tofa remove GitHub:you
Passphrase: ********
Remove "GitHub:you"? [y/N] n
Aborted.

Notes

  • The prompt always defaults to No — pressing Enter alone keeps the entry.
  • Ambiguous prefixes (matching multiple entries) print the candidates and exit non-zero without modifying anything.
  • The vault is rewritten atomically.

See also

tofa rename

Rename an account. Takes the current id and the new id as two positional arguments.

Synopsis

tofa rename

Examples

$ tofa rename GitHub:you GitHub:me
Passphrase: ********
✓ renamed GitHub:you → GitHub:me

Notes

  • Refuses if the target id already exists. Remove the conflicting entry first if that's what you want.
  • The underlying TOTP secret is unchanged — you keep the same codes.

See also

  • tofa list — confirm the new id is what you expected.
  • tofa remove — if you don't need the entry anymore.

tofa qr

Print a QR code for one account in the terminal, or export QRs for one or all accounts to disk.

Synopsis

tofa qr [FLAGS]

Flags

FlagDescription
--all <ALL>Export all accounts as a migration QR
--multi <MULTI>Emit one otpauth:// QR per entry instead of a single migration QR. Requires --all and --output-dir. Preserves period/algorithm/digits for every entry — use this when the migration format would refuse because the selection mixes 30s and non-30s entries
--output <PATH>Save QR as PNG instead of displaying in terminal (single-QR modes)
--output-dir <DIR>Directory to write per-entry PNGs into when using --multi

Examples

Print one account's QR in the terminal (scannable from a phone camera held up to the screen):

$ tofa qr GitHub:you
Passphrase: ********
█▀▀▀▀▀█ ▀▀█▀▀ █▀▀▀▀▀█
█ ███ █  ▀ ▀▀ █ ███ █
█ ▀▀▀ █ ▀█▀▄  █ ▀▀▀ █
▀▀▀▀▀▀▀ ▀ ▀▀▀ ▀▀▀▀▀▀▀
...

Save as a PNG instead of printing:

$ tofa qr GitHub:you --output github-you.png
Passphrase: ********
QR saved to github-you.png

Export every account as a single migration QR (Google Authenticator format — scan it with the Authenticator app to import everything at once):

$ tofa qr --all --output migration.png
Passphrase: ********
QR saved to migration.png

If the selection mixes 30s and non-30s entries, the migration format can't carry them. Use --multi to write one otpauth:// PNG per entry instead — period, digits, and algorithm are preserved per file:

$ tofa qr --all --multi --output-dir ~/tofa-qrs
Passphrase: ********
Wrote 12 QR PNG(s) to /Users/you/tofa-qrs

Filenames are zero-padded and sanitized (01-GitHub_you.png, 02-Discord_you.png, …) so a directory listing matches the in-vault order.

Notes

  • --all is a switch; the <ALL> placeholder above is a clap quirk for bool flags.
  • Terminal QRs use Unicode block characters and need a font with full block support (most modern fonts do).
  • The migration format is the same one Google Authenticator uses — any reader that handles otpauth-migration:// will accept the result.
  • --multi is the right mode when the migration format refuses your selection (mixed periods) or when the receiving app prefers per-account QRs. The output round-trips through tofa import — zip the directory (zip -r tofa-qrs.zip ~/tofa-qrs) and import the archive.

See also

  • tofa export — JSON dump for offline backups.
  • tofa add with --qr — the inverse: read a QR image.

tofa rekey

Change the vault passphrase. Asks for the current passphrase, then the new one twice.

Synopsis

tofa rekey

Examples

$ tofa rekey
Current passphrase: ********
New passphrase: ************
Confirm new passphrase: ************
✓ vault re-encrypted

Notes

  • The vault is rewritten atomically: temp file then rename. If anything goes wrong mid-write, the old vault is intact.
  • A fresh nonce and a fresh KDF salt are generated, so the new ciphertext is unrelated to the old one even if accounts are unchanged.
  • There is no "set a new passphrase without knowing the old one" — that would defeat the encryption.

See also

tofa export

Dump every account in the vault as plain text — either re-importable JSON (the default) or a list of otpauth:// URIs. Both formats contain your secrets unencrypted. Use only for backups or migration.

Synopsis

tofa export [FLAGS]

Flags

FlagDescription
--output <PATH>Write to a file instead of stdout
--format <FORMAT>Output format. Defaults to json for backwards compatibility

Examples

To stdout:

$ tofa export
Passphrase: ********
{
  "version": 1,
  "entries": [
    { "id": "GitHub:you", "name": "GitHub:you", "secret": "JBSWY3DPEHPK3PXP", "issuer": "GitHub", ... }
  ]
}

To a file (recommended — set restrictive permissions immediately):

$ tofa export --output ~/tofa-backup.json
Passphrase: ********
✓ wrote ~/tofa-backup.json (3 accounts)
$ chmod 600 ~/tofa-backup.json

Pipe into another tool (e.g., jq):

tofa export | jq '.entries | length'

Export as a plain-text URI list (one otpauth:// per line). The default output filename becomes tofa-export-<date>.txt:

$ tofa export --format uris
Passphrase: ********
3 account(s) exported to tofa-export-2026-05-07.txt

That file round-trips back through tofa import — and also through the desktop app's drop / picker and the TUI's file picker, so you can hand it to a teammate (over a secure channel) and they can import in one step.

Notes

  • The output is not encrypted. Treat it like a password file: chmod 600, store on encrypted media, delete after use.
  • JSON output has a version field and is the safest long-term backup shape — it carries every entry's metadata.
  • The URI-list format (--format uris) preserves period / digits / algorithm too (encoded in each URI's query string), but is friendlier for piping or sharing one URI at a time.

See also

tofa import

Import accounts from any file format other authenticators (and TOFA itself) emit. One unified dispatcher handles every shape: single- and multi-QR images, Google Authenticator migration QRs, JSON / CSV / TXT exports from the major mobile and desktop authenticators, and zip archives mixing any of the above.

Synopsis

tofa import

Supported formats

SourceExtension(s)
Single-QR image.png .jpg .gif .bmp .webp .tiff
Multi-QR image (e.g. backup printout)same as above
Google Authenticator migration QRsame as above (or pasted as text)
Aegis / andOTP / 2FAS / Bitwarden / FreeOTP+ / Raivo / native tofa export.json .2fas
KeePassXC CSV.csv
Ente Auth plain-text URI list.txt
Zip archive of any of the above (recursive).zip

The dispatch is by extension, then by content where the extension is ambiguous. A .txt file containing a single otpauth-migration:// URI is expanded into every account it carries.

Examples

Aegis or andOTP JSON export:

$ tofa import ~/Downloads/aegis-export.json
Passphrase: ********
Imported 12 account(s).

A printout (or screenshot) showing many QRs at once:

$ tofa import ~/Downloads/backup-printout.png
Passphrase: ********
Imported 11 account(s).

A Google Authenticator export QR:

$ tofa import ~/Downloads/migration.png
Passphrase: ********
Imported 8 account(s).

A zip from tofa-app's Save All button — round-trips your backup without manually unzipping:

$ tofa import ~/Downloads/tofa-qrs.zip
Passphrase: ********
Imported 12 account(s).

A plain-text list of otpauth:// URIs (Ente Auth's export format, or the output of tofa export --format uris):

$ tofa import ~/Downloads/tofa-export-2026-05-07.txt
Passphrase: ********
Imported 5 account(s).

Notes

  • Skips entries that already exist in the vault (matched on name + secret). Re-importing the same file is safe and reports the skip count.
  • Zip extraction is in-memory: bytes are never written to disk during import, so slip-path attacks aren't a concern.
  • The source file is plain text — delete it after import (shred -u on Linux, rm -P on macOS).

See also

tofa scan

Capture every connected display and import every QR code visible — single accounts, Google Authenticator migration QRs, or a printout showing many at once. A spinner reports per-pass progress while the scanner runs.

Experimental. Real-world Retina captures of dense backup printouts can occasionally miss a QR that lands at the edge of the detector's threshold (typically 0–1 out of ~10). If a code is missing, rerun the scan or import that one separately via tofa import <file>. The CLI prints this caveat on every run.

Synopsis

tofa scan [FLAGS]

Flags

FlagDescription
--name <NAME>Override the account name (only applied when exactly one entry is found)

Examples

Show a TOTP QR somewhere on screen (browser, password manager, etc.), then:

$ tofa scan
⚠  Experimental — screen scan may miss QR codes at the edge of rqrr's
   detection threshold. If a code is missing, rerun the scan or import
   that one separately.
⠹ screen 1/2 • pass @ 3840px • 7 found
Passphrase: ********
Imported 11 account(s) from 2 screen(s).

Override the account name (only applied when the scan yields exactly one entry):

$ tofa scan --name "GitHub:work"
Passphrase: ********
Imported 1 account(s) from 1 screen(s).

Re-running the scan with QRs already in the vault — duplicates are counted and skipped, so it's safe to retry:

$ tofa scan
...
Imported 0 account(s) from 1 screen(s) (3 duplicate(s) skipped).

How it works

  • Capture. macOS uses screencapture -D N per display (one PNG per monitor). Wayland uses grim; X11 uses scrot -m and falls back to gnome-screenshot. The CLI captures every connected display; multi-monitor setups are first-class.
  • Decode. Each capture is run through a small resolution ladder (native ≤3840 → 1920 with both Lanczos3 and Triangle filters → 1280 → 960). Filter diversity helps marginal QRs decode that would otherwise sit just below the detector's threshold. Early termination stops once two consecutive passes find nothing new.
  • Progress. A stderr spinner shows the current screen, current rescale width, and running count of decoded URIs.

Notes

  • On first macOS run, the system prompts for Screen Recording permission. Grant it in System Settings → Privacy & Security if the scan returns nothing.
  • Linux requires one of grim (Wayland), scrot (X11), or gnome-screenshot (single-display fallback) on $PATH.
  • All visible QRs are imported. Migration QRs are expanded into their constituent accounts; vCards / URLs / other QR payloads are silently ignored.
  • Accounts already in the vault are deduped on (name, secret) and reported as N duplicate(s) skipped — re-running a scan never creates duplicate rows.

See also

  • tofa cam — webcam-based scanner.
  • tofa import — same dispatcher, but for files (single-QR, multi-QR, zip, JSON, …).
  • tofa add--qr <PATH> for a single QR image on disk.

tofa cam

Open a browser-based webcam scanner and add the first QR detected.

Synopsis

tofa cam [FLAGS]

Flags

FlagDescription
--camera <INDEX>Camera index passed to the browser (default: 0)
--name <NAME>Override the account name (default: derived from QR metadata)

Examples

Default camera:

$ tofa cam
Passphrase: ********
Open http://127.0.0.1:54321 in your browser…
Added GitHub:you
Current code: 482 913  (21s)

Pick a specific camera (e.g., the second one):

$ tofa cam --camera 1

A migration QR captured with the camera expands into all of its accounts in one shot:

$ tofa cam
...
Imported 3 account(s).

Notes

  • The browser scanner runs locally on a random port — nothing leaves your machine.
  • Grant the browser permission to use the camera when prompted.
  • Once a QR decodes, the page closes itself and tofa continues with the detected account.
  • Like every TOFA import surface, cam dedups on (name, secret). Pointing the camera at a QR you've already imported errors out for a single account, or reports N duplicate(s) skipped for a migration QR — never silently double-adds.

See also

tofa completions

Print shell completions for tofa. Pipe the output into your shell's completion directory.

Synopsis

tofa completions

Examples

zsh:

tofa completions zsh > ~/.zsh/completions/_tofa

bash:

tofa completions bash > /usr/local/etc/bash_completion.d/tofa

fish:

tofa completions fish > ~/.config/fish/completions/tofa.fish

Notes

  • Supported shells: bash, zsh, fish, powershell, elvish.
  • Output is plain text — safe to redirect to a file.

See also

tofa destroy

Permanently delete the vault file. There is no undo.

Synopsis

tofa destroy

Examples

$ tofa destroy
This will permanently delete /Users/you/Library/Application Support/tofa/vault.enc.
Type "destroy" to confirm: destroy
✓ vault destroyed

Anything other than the literal word destroy aborts:

$ tofa destroy
Type "destroy" to confirm: yes
Aborted.

Notes

  • Irreversible. There is no encrypted backup, no soft-delete, no trash.
  • The file is removed with a normal unlink. If you want to wipe the underlying blocks too, run shred -u (Linux) or rm -P (macOS) on the vault path before calling destroy.

See also

  • tofa init — start fresh after destroy.
  • tofa rekey — change passphrase without losing data.

Importing from Aegis or andOTP

tofa import reads the JSON export format used by Aegis and andOTP — and also 2FAS, Bitwarden, FreeOTP+, Raivo, KeePassXC CSV, Ente Auth's plain text URI list, single- and multi-QR images, Google Authenticator migration QRs, and zip archives mixing any of the above. See the tofa import reference for the full list.

Aegis (Android)

  1. In Aegis: Settings → Import & Export → Export → Plain text.
  2. Transfer the file to your machine (scp, AirDrop, USB).
  3. Import:
tofa import ~/Downloads/aegis-export.json

The file is plain text — delete it after a successful import:

shred -u ~/Downloads/aegis-export.json   # Linux
rm -P ~/Downloads/aegis-export.json      # macOS

andOTP

Use Backups → Plain-text → Backup. Same import command. Same caveat about the export being plain text.

Migration QR codes

Google Authenticator exports as a QR-encoded URL (otpauth-migration://). Three ways to bring it in:

tofa scan                                  # capture every screen, decode all QRs
tofa import ~/Downloads/migration.png      # if you saved the QR to disk
tofa add --qr ~/Downloads/migration.png    # equivalent, single-account flow

tofa scan is the fastest path when the QR is on screen — it picks up multi-account migration QRs and printout grids in one pass.

Backup printouts (multi-QR images)

If you exported your vault from another tool as a printable page of QRs, pass the PNG / JPG straight to tofa import — the dispatcher finds and imports every QR on the page in one shot. This is also how the desktop app's Save All zip round-trips back into a vault.

tofa import ~/Downloads/backup-printout.png

Moving an account to another authenticator

Sometimes you need to move one account out of TOFA without exporting the whole vault — for example, sharing a service account with a teammate, or migrating a single login to your phone. The otpauth:// URI is the lingua franca: every authenticator app can import it, and TOFA can both emit and accept it.

Single account

Copy the URI to your clipboard:

tofa code GitHub:you --uri --copy

The clipboard now holds an otpauth://totp/... URI carrying the secret, period, digits, and algorithm. Paste it into the receiving app's add account flow.

In the TUI, select the entry and press u. In the menu bar app, open the entry's detail view and click the URI button.

Many accounts as a list

Export the selected entries (or the whole vault) as a plain-text URI list:

tofa export --format uris --output accounts.txt

The file has one otpauth:// per line. Most authenticators will accept it directly; otherwise re-import into another tofa install with:

tofa import accounts.txt

In the TUI, open the export modal (e from the list) and press u to write a date-stamped .txt to your home directory. In the menu bar app, open Export QR and click Save URI list (.txt).

Caveats

  • The URI contains the secret — treat the clipboard, file, or pasted message as you would a password. Clear the clipboard after use; delete the file.
  • The receiving authenticator gets an exact copy of the entry, so both apps will produce the same codes from that point on. Decide ahead of time whether you want to remove the entry from TOFA after the move.

Copying codes to the clipboard

tofa code writes the 6-digit OTP to stdout and the countdown to stderr, which makes it pipe-friendly. There's also a built-in --copy flag that handles the platform clipboard for you.

Built-in flag

tofa code GitHub:you --copy

Works on macOS, Linux (X11 and Wayland), and Windows.

Manual piping

If you'd rather use your own tool — e.g., to integrate with a password manager — combine --raw (no separator space) with the platform's clipboard binary:

macOS

tofa code GitHub:you --raw | pbcopy

Linux (X11)

tofa code GitHub:you --raw | xclip -selection clipboard

Linux (Wayland)

tofa code GitHub:you --raw | wl-copy

Avoid leaving the code in the clipboard

The TOTP expires every 30 seconds anyway, but if you want to clear sooner:

tofa code GitHub:you --raw | pbcopy && (sleep 15; pbcopy < /dev/null)

Scripting with TOFA_PASSPHRASE

For non-interactive use (CI, scripts, password manager hooks), set TOFA_PASSPHRASE so tofa does not prompt:

export TOFA_PASSPHRASE="$(security find-generic-password -s tofa -w)"
tofa code GitHub:you

tofa prints a warning to stderr when it reads this variable. Never bake your passphrase into a shell history or a committed file. Use a system secret store instead:

  • macOS Keychain: security find-generic-password -s tofa -w
  • Linux Secret Service: secret-tool lookup service tofa
  • 1Password CLI: op read "op://Personal/tofa/passphrase"

A login-time script

#!/usr/bin/env bash
# ~/bin/otp
set -eu
export TOFA_PASSPHRASE="$(security find-generic-password -s tofa -w)"
tofa code "$1" --raw | tr -d '\n' | pbcopy
echo "copied OTP for $1"

Make it executable, then otp GitHub:you copies the current code.

Pairing with watchdog tools

Because tofa code --raw exits cleanly on success and non-zero on missing account or bad passphrase, it composes well with set -e scripts and CI guards.

Shell completions

tofa completions <shell> prints completions to stdout. Install them once and forget.

zsh

mkdir -p ~/.zsh/completions
tofa completions zsh > ~/.zsh/completions/_tofa

Add to ~/.zshrc (if not already there):

fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit

bash

tofa completions bash > /usr/local/etc/bash_completion.d/tofa

fish

tofa completions fish > ~/.config/fish/completions/tofa.fish

powershell

tofa completions powershell | Out-String | Invoke-Expression

(Add the same line to your PowerShell profile to persist.)

See also

tofa completions — flag reference.

Security model

Threat model

tofa protects your TOTP secrets from:

  • Disk theft — the vault is AES-256-GCM-encrypted; without the passphrase the file is opaque.
  • Casual snooping — secrets never appear in plain text on disk during normal operation; only export produces a plain-text dump (and warns).

tofa does not protect from:

  • A keylogger or process running under your user — it sees the passphrase.
  • A malicious binary masquerading as tofa — verify your install source.

Cryptography

PrimitiveChoice
Key derivationArgon2id (m = 64 MiB, t = 3, p = 1)
EncryptionAES-256-GCM with a fresh random 96-bit nonce per write
IntegrityBuilt into GCM (16-byte auth tag)

The salt is stored alongside the ciphertext. Each save uses a fresh nonce, so re-saving the same vault produces a different ciphertext.

Atomic writes

The vault is written to a temp file in the same directory and then renamed into place. A crash mid-write leaves either the old vault or the new vault intact — never a half-written file.

Memory hygiene

The decrypted passphrase is held in a Zeroizing buffer with a 10-minute TTL. On lock (manual, timeout, or process exit) the buffer is zeroed.

Unsigned build

The macOS app is not yet notarized by Apple. Releases are built and distributed unsigned, which means macOS Gatekeeper will quarantine the app on first launch and refuse to open it.

To allow it through, run once after install:

xattr -dr com.apple.quarantine /Applications/tofa.app

This removes the quarantine flag that macOS adds to apps downloaded from the internet. Alternatively, right-click the app in Finder, choose Open, and confirm the dialog — same effect.

Notarization is on the roadmap. Until then, you can audit the release workflow that produces these binaries, and run cargo tauri build locally from the same commit to produce your own.

Reporting a vulnerability

Open a private security advisory at https://github.com/stratif-io/tofa/security/advisories/new.