tofa

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.

This site documents the command-line interface and its companion TUI. For the macOS menu bar app see 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

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.

From crates.io

cargo install tofa

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.
importImport from JSON or a migration QR.
scanCapture the screen and read a visible QR.
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 code to clipboard
--watch <WATCH>Refresh every second until Ctrl+C

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

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: ********
✓ added GitHub:you
✓ added Discord:you
✓ added Slack:work
imported 3 accounts

Notes

  • Exactly one of --secret, --uri, or --qr is required.
  • --secret requires --name; --uri and --qr derive the name themselves (override with --name).
  • 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 duplicate account ids.

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
--output <PATH>Save QR as PNG instead of displaying in terminal

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: ********
✓ wrote 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: ********
✓ wrote migration.png (3 accounts)

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.

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 JSON. Use only for backups or migrating to another tool — the output contains your secrets unencrypted.

Synopsis

tofa export [FLAGS]

Flags

FlagDescription
--output <PATH>Write to a file instead of stdout

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'

Notes

  • The output is not encrypted. Treat it like a password file: chmod 600, store on encrypted media, delete after use.
  • Format is stable JSON with a version field — safe to keep around for later import.

See also

tofa import

Import accounts from a JSON file (Aegis, andOTP, or tofa export format) or from a migration QR image.

Synopsis

tofa import

Examples

From a JSON export:

$ tofa import ~/Downloads/aegis-export.json
Passphrase: ********
✓ added GitHub:you
✓ added Discord:you
- skipped Slack:work (already in vault)
imported 2 of 3 entries

From a migration QR (Google Authenticator export):

$ tofa import ~/Downloads/migration.png
Passphrase: ********
✓ added GitHub:you
✓ added Discord:you
imported 2 entries

Notes

  • Skips entries whose id is already in the vault — re-importing the same file is safe.
  • Accepts JSON formats from Aegis, andOTP, and tofa export interchangeably.
  • The source file is plain text — delete it after import (shred -u on Linux, rm -P on macOS).

See also

tofa scan

Capture the entire screen, scan it for QR codes, and add the first account found.

Synopsis

tofa scan [FLAGS]

Flags

FlagDescription
--name <NAME>Override the account name (default: derived from QR metadata)

Examples

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

$ tofa scan
Passphrase: ********
✓ added GitHub:you (issuer=GitHub, label=you)

Override the account name:

$ tofa scan --name "GitHub:work"
Passphrase: ********
✓ added GitHub:work

Notes

  • macOS only (uses ScreenCaptureKit). On Linux, save the QR to a file and use tofa add --qr instead.
  • On first use, macOS prompts for Screen Recording permission. Grant it in System Settings → Privacy & Security if the scan returns nothing.
  • If multiple QRs are visible, the first one decoded wins.

See also

  • tofa cam — webcam-based scanner.
  • tofa add--qr <PATH> for QR images 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

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

$ tofa cam --camera 1

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.

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.

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://). Capture the screen showing it and run tofa scan, or save the QR as a PNG and run tofa add --qr migration.png.

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.

Reporting a vulnerability

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