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
From source (recommended)
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
| OS | Default 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
tofa init·tofa rekey·tofa destroy- Security model for the cryptographic details.
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:
| Flag | Description |
|---|---|
--vault <PATH> | Override the vault path. Reads from TOFA_VAULT env var as fallback. |
--help, -h | Print help. |
--version, -V | Print the version. |
Environment variables
| Variable | Effect |
|---|---|
TOFA_VAULT | Default vault path when --vault is not given. |
TOFA_PASSPHRASE | If set, used instead of an interactive prompt. Avoid in production — tofa prints a warning to stderr when it reads this. |
Commands
| Command | Purpose |
|---|---|
init | Create a new encrypted vault. |
list | List every account. |
code | Print the current TOTP code for one account. |
add | Add an account from secret, URI, or QR. |
remove | Remove an account. |
rename | Rename an account. |
qr | Print a QR code for one account or all. |
rekey | Change the vault passphrase. |
export | Dump every account as JSON. |
import | Import from JSON or a migration QR. |
scan | Capture the screen and read a visible QR. |
cam | Open the webcam and wait for a QR. |
completions | Print shell completions. |
destroy | Permanently 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
0on success. - Refuses to overwrite an existing vault file. Move or delete it first if you
really want to start over (consider
destroyfor that). - The passphrase is asked twice and must match. There is no recovery — losing the passphrase makes the vault unreadable forever.
See also
tofa rekey— change the passphrase later.tofa destroy— wipe the vault.- Vault & passphrase — broader explanation.
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
| Flag | Description |
|---|---|
--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.
--codesis 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
| Flag | Description |
|---|---|
--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,
tofalists the candidates and exits non-zero. --raw,--copy,--watchare all switches; the<RAW>etc. placeholders in the auto-generated table above are a clap quirk for booleans.- Exit code
1on missing account, wrong passphrase, or ambiguous prefix.
See also
tofa list— see all accounts at once.- Recipe: clipboard — copy patterns by platform.
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
| Flag | Description |
|---|---|
--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--qris required. --secretrequires--name;--uriand--qrderive 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
0on 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 list— find the right id first.tofa rename— keep the entry but rename it.
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
| Flag | Description |
|---|---|
--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
--allis 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 addwith--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
- Vault & passphrase — broader explanation.
- Security model — Argon2id and AES-256-GCM details.
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
| Flag | Description |
|---|---|
--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
versionfield — safe to keep around for later import.
See also
tofa import— read this format back.- Recipe: import from Aegis / andOTP — same JSON shape.
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 exportinterchangeably. - The source file is plain text — delete it after import (
shred -uon Linux,rm -Pon macOS).
See also
tofa export— produce a JSON export.- Recipe: import from Aegis / andOTP — step-by-step migration.
tofa scan
Capture the entire screen, scan it for QR codes, and add the first account found.
Synopsis
tofa scan [FLAGS]
Flags
| Flag | Description |
|---|---|
--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 --qrinstead. - 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
Open a browser-based webcam scanner and add the first QR detected.
Synopsis
tofa cam [FLAGS]
Flags
| Flag | Description |
|---|---|
--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
tofacontinues with the detected account.
See also
tofa scan— screen capture variant (no camera needed).tofa add --qr— for an image on disk.
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
- Recipe: completions setup — full
install instructions per shell, including
fpathsetup for zsh.
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, runshred -u(Linux) orrm -P(macOS) on the vault path before callingdestroy.
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)
- In Aegis: Settings → Import & Export → Export → Plain text.
- Transfer the file to your machine (
scp, AirDrop, USB). - 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
exportproduces 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
| Primitive | Choice |
|---|---|
| Key derivation | Argon2id (m = 64 MiB, t = 3, p = 1) |
| Encryption | AES-256-GCM with a fresh random 96-bit nonce per write |
| Integrity | Built 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.