README
¶
pass — Windows Password Store
A minimal, secure, Git-backed password manager for Windows. Inspired by passwordstore.org, built natively for PowerShell with no Linux tools required.
██████╗ █████╗ ███████╗███████╗
██╔══██╗██╔══██╗██╔════╝██╔════╝
██████╔╝███████║███████╗███████╗
██╔═══╝ ██╔══██║╚════██║╚════██║
██║ ██║ ██║███████║███████║
╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝
What it does
pass stores each of your passwords as its own small encrypted file inside
~\.password-store\. The whole folder is also a Git repository, so you can
push it to a private GitHub repo and clone it on any new PC — your passwords
travel with you.
~\.password-store\
├── Email\
│ ├── gmail.age
│ └── work.age
├── Finance\
│ └── bank.age
└── Social\
└── twitter.age
Every file is encrypted with age using your
personal X25519 keypair. Decryption requires your passphrase, which you type
once per terminal session. After that, pass remembers it in memory until you
close the terminal or run pass lock.
How the encryption works
┌──────────────────────────────────────────────────────────┐
│ SETUP (once) │
│ │
│ pass init → generates X25519 keypair │
│ │ │
│ ├─ private key → encrypted with your │
│ │ passphrase (scrypt KDF) │
│ │ saved as ~/.age-identity │
│ │ │
│ └─ public key → saved in │
│ ~/.password-store/ │
│ .age-recipients │
│ │
├──────────────────────────────────────────────────────────┤
│ EVERY TIME YOU INSERT / GENERATE │
│ │
│ plaintext → encrypted with public key → .age file │
│ (no passphrase needed to write) │
│ │
├──────────────────────────────────────────────────────────┤
│ EVERY TIME YOU READ │
│ │
│ passphrase → unlocks private key → decrypts file │
│ (asked once per session, cached in memory) │
└──────────────────────────────────────────────────────────┘
Cryptographic primitives used:
- Key agreement: X25519 (Curve25519)
- Encryption: ChaCha20-Poly1305 (authenticated)
- Passphrase KDF: scrypt
If you forget your passphrase, the private key cannot be recovered and all entries become permanently inaccessible. Keep your passphrase somewhere safe.
Entry format
Each decrypted entry is plain text. The first line is always the password. Remaining lines are optional named fields:
MySuperStr0ngPassword
username: john@gmail.com
url: https://gmail.com
notes: personal account, recovery email is work@example.com
You can add any field name you like. pass show --field <name> lets you
extract a single field without printing the rest.
Tech stack
| Component | Library |
|---|---|
| Language | Go 1.21+ |
| CLI framework | cobra |
| Encryption | filippo.io/age |
| Terminal input | golang.org/x/term |
| TUI framework | Bubble Tea |
| TUI components | Bubbles |
| TUI styling | Lip Gloss |
| Git | git.exe (external, via exec.Command) |
Requirements
| Requirement | Notes |
|---|---|
| Windows 10 / 11 | Tested on Windows 11 |
| PowerShell 5.1+ or Windows Terminal | Any modern terminal works |
| Git for Windows | Only needed for sync features |
Go does not need to be installed to run pass.exe — it is a single
self-contained binary.
Setup
Option A — Download release binary (recommended)
- Go to the Releases page
- Download
pass_<version>_windows_amd64.zip(orarm64for ARM devices) - Extract
pass.exeto a folder on yourPATH(e.g.C:\tools\)
Option B — Install with go install
# Requires Go 1.21+ → https://go.dev/dl/
go install github.com/ThomasTheCoder198/PassStore/cmd/pass@latest
The binary is placed in $env:GOPATH\bin (usually %USERPROFILE%\go\bin), which is
already on your PATH if you installed Go with the official installer.
Option C — Build from source
# 1. Install Go 1.21+ → https://go.dev/dl/
# 2. Clone this repo
git clone https://github.com/ThomasTheCoder198/PassStore "C:\tools\pass-src"
cd "C:\tools\pass-src"
# 3. Build
go build -o pass.exe .
Add to PATH (if using Option A)
# Add C:\tools to your user PATH permanently
[Environment]::SetEnvironmentVariable(
"PATH",
"$env:PATH;C:\tools",
"User"
)
Restart your terminal. Verify:
pass --help
First-time initialization
# Initialize the store and a git repository inside it
pass init --git
You will be prompted to create a passphrase. This passphrase protects your private key. Choose something strong and memorable — there is no reset.
Enter passphrase for identity file: ████████████
Repeat passphrase: ████████████
Store initialized. Identity: ~/.age-identity
Two files are created:
| File | Purpose |
|---|---|
~\.age-identity |
Your private key, encrypted with your passphrase |
~\.password-store\.age-recipients |
Your public key (used to encrypt entries) |
Syncing to another PC
Push from your first PC
# Connect to a private GitHub repo
pass git remote add origin https://github.com/<YOUR_USERNAME>/passwords.git
pass git push -u origin main
Clone on a new PC
# 1. Install pass.exe (see Setup above)
# 2. Clone your store
pass git clone https://github.com/<YOUR_USERNAME>/passwords.git "$env:USERPROFILE\.password-store"
# 3. Copy your identity file from the old PC to this PC
# Options: USB drive, encrypted cloud storage, SCP, etc.
# Destination: ~\.age-identity
# 4. Done — type your passphrase on the first command
pass ls
Security note: Keep
~\.age-identityprivate. Anyone with this file AND your passphrase can decrypt your entire store. The.age-recipientsfile (public key) inside the store is safe to expose.
Migrating to a new PC (full walkthrough)
This covers every step from old PC to new PC, including what to do if you are replacing the old machine entirely.
Step 1 — On the old PC: make sure everything is pushed
pass git status # confirm nothing is uncommitted
pass git push # push latest entries to GitHub
Step 2 — On the old PC: export your identity file
Your private key lives at ~\.age-identity. You need to get this file onto the
new PC. Choose one method:
Option A — USB drive (safest)
Copy-Item "$env:USERPROFILE\.age-identity" "E:\age-identity.bak"
Transfer the USB to the new PC, then delete the file from the drive when done.
Option B — Encrypted archive (if USB unavailable)
# Compress and password-protect with 7-Zip (install from 7-zip.org)
& "C:\Program Files\7-Zip\7z.exe" a -p "$env:USERPROFILE\Desktop\identity.7z" "$env:USERPROFILE\.age-identity"
# Upload identity.7z to a private location, download on new PC, then delete it
Do NOT transfer
~\.age-identityover plain email or unencrypted cloud storage. The file is already encrypted with your passphrase, but a copy sitting in email or cloud history is a permanent exposure risk.
Step 3 — On the new PC: install pass
Download or build pass.exe (see Setup) and add it to your PATH.
Step 4 — On the new PC: clone the store
pass git clone https://github.com/<YOUR_USERNAME>/passwords.git "$env:USERPROFILE\.password-store"
Step 5 — On the new PC: place your identity file
# Copy from wherever you transferred it to
Copy-Item "E:\age-identity.bak" "$env:USERPROFILE\.age-identity"
# Restrict permissions to your user account only
icacls "$env:USERPROFILE\.age-identity" /inheritance:r /grant:r "${env:USERNAME}:(R,W)"
Step 6 — Verify
pass ls # should show your entry tree
pass show Email/gmail # enter passphrase → should show password
That is all. Your full store is now available on the new PC.
Changing your passphrase
There is no pass repassphrase command. To change your passphrase, re-encrypt
the identity file manually:
# 1. Unlock with the OLD passphrase and export the raw identity
# (pass show will prompt for the old passphrase and cache it)
pass ls # just to unlock the session
# 2. Re-encrypt the identity file with the NEW passphrase
# Run this small PowerShell snippet — it uses the cached identity in the
# current session, so it does not ask for the old passphrase again.
# Close and reopen the terminal when done.
Because pass does not expose a repassphrase command yet, the safest manual
procedure is:
# 1. Note your current identity path
$idPath = "$env:USERPROFILE\.age-identity"
# 2. Back up the old identity file before touching it
Copy-Item $idPath "$idPath.bak"
# 3. Delete the store and identity
Remove-Item -Recurse -Force "$env:USERPROFILE\.password-store"
Remove-Item -Force $idPath
# 4. Re-initialize with your NEW passphrase
pass init --git
# 5. Re-add your remote and force-push (the new store has a new keypair)
pass git remote add origin https://github.com/<YOUR_USERNAME>/passwords.git
pass git push --force # ← replaces the old encrypted store on GitHub
# 6. Delete the backup
Remove-Item "$idPath.bak"
Why force-push? The new
pass initcreates a new keypair. All existing.agefiles are encrypted to the old public key and unreadable with the new private key. The new store starts empty — you will need to re-insert your passwords. If that is too disruptive, contact support or wait for a futurerepassphrasecommand that re-encrypts all entries in one step.
Day-to-day sync workflow
# At the start of the day (pull latest from any other PC)
pass git pull
# After adding or changing passwords (push to GitHub)
pass git push
Auto-commits happen on every mutation, so you only need to push manually. Pull before you push if you use multiple PCs to avoid conflicts.
Usage
Session passphrase
The first command that reads a password will ask for your passphrase. After that, the unlocked key is cached in memory for the life of the process.
pass show Email/gmail
# Enter passphrase: ████ ← asked once
# myGmailPassword!
pass show Finance/bank
# (no prompt — key already cached)
# mySuperBankPass99
Run pass lock or close the terminal to clear the cache.
Commands
pass init
Initialize a new password store.
pass init # store only
pass init --git # store + git repository (recommended)
pass ls
List all entries as a tree. Optionally scope to a subfolder.
pass ls
pass ls Email
Password Store
├── Email
│ ├── gmail
│ └── work
├── Finance
│ └── bank
└── Social
└── twitter
pass show
Decrypt and display an entry. Aliases: pass with just a name also works when
the name is unambiguous.
pass show Email/gmail # print all content
pass show Email/gmail -f username # print only the username field
pass show Email/gmail -f url # print only the url field
pass show Email/gmail -c # copy password to clipboard (clears in 45s)
Flags:
| Flag | Short | Description |
|---|---|---|
--field <name> |
-f |
Print only the value of the named field |
--clip |
-c |
Copy the password (first line) to clipboard; auto-clears after 45 seconds |
pass insert
Add a new password entry. You will be prompted to type the password (hidden input, confirmed twice).
pass insert Email/gmail # single-line password
pass insert Email/gmail --multiline # multi-line (end with empty line)
pass insert Email/gmail --force # overwrite existing entry
Multiline example — type each field, then press Enter on an empty line:
Enter contents (end with empty line):
myGmailPassword!
username: john@gmail.com
url: https://gmail.com
notes: recovery phone: +1-555-0100
← empty line ends input
Flags:
| Flag | Short | Description |
|---|---|---|
--multiline |
-m |
Read multiple lines (password + fields) until empty line |
--force |
Overwrite if entry already exists |
pass generate
Generate a cryptographically random password using crypto/rand.
pass generate Social/twitter # 20-char password with symbols
pass generate Social/twitter 32 # 32-char password
pass generate Social/twitter --no-symbols # alphanumeric only
pass generate Social/twitter -c # generate and copy to clipboard
pass generate Social/twitter --force # overwrite existing
Default charset: a-zA-Z0-9 + !@#$%^&*()-_=+[]{}|;:,.<>?
Flags:
| Flag | Short | Description |
|---|---|---|
--no-symbols |
Use only letters and numbers | |
--clip |
-c |
Copy generated password to clipboard (clears in 45s) |
--force |
Overwrite if entry already exists |
pass edit
Decrypt an entry, open it in your editor, then re-encrypt and save.
pass edit Email/gmail
Opens $env:EDITOR if set, otherwise falls back to notepad.exe. The
plaintext is written to a temp file, opened in the editor, and deleted after
you close the editor. Auto-commits the change.
Set your preferred editor:
$env:EDITOR = "code --wait" # VS Code
$env:EDITOR = "notepad++" # Notepad++
$env:EDITOR = "vim" # Vim (if installed)
pass rm
Remove an entry or directory. Aliases: remove, delete.
pass rm Email/gmail # remove single entry (prompts for confirmation)
pass rm Email/gmail -f # remove without confirmation
pass rm Email -r # remove entire folder recursively
pass rm Email -r -f # remove folder without confirmation
Flags:
| Flag | Short | Description |
|---|---|---|
--recursive |
-r |
Remove a directory and everything inside it |
--force |
-f |
Skip the confirmation prompt |
pass mv
Move or rename an entry. Alias: rename.
pass mv Email/gmail Email/gmail-old # rename
pass mv Email/gmail Personal/gmail # move to different folder
Creates the destination folder automatically if it does not exist.
pass cp
Copy an entry to a new name.
pass cp Finance/bank Finance/bank-backup
pass find
Find entries by name (case-insensitive substring match).
pass find gmail # finds Email/gmail
pass find bank # finds Finance/bank, Finance/bank-backup
pass grep
Search the decrypted content of every entry for a string. Requires your passphrase.
pass grep john@gmail.com # find entries containing this email
pass grep "Chase Bank" # find entries with this text in notes
Output format: <entry-name>: <matching line>
pass git
Full proxy to git with the store as the working directory. Any git
subcommand works.
pass git status
pass git push
pass git pull
pass git log --oneline
pass git remote add origin https://github.com/<YOUR_USERNAME>/passwords.git
pass git clone https://github.com/<YOUR_USERNAME>/passwords.git
Every mutating command (
insert,generate,edit,rm,mv,cp) automatically creates a git commit when the store is a git repository. The commit message describes the operation, e.g.:Add password for Email/gmail.
pass lock
Clear the cached passphrase from the current session. The next command that needs decryption will prompt for it again.
pass lock
# Session locked.
pass ui
Launch the interactive TUI. Also launched by running pass with no arguments.
pass ui
pass # same thing
┌─────────────────────────────────────────────────────────┐
│ .-""-. ██████╗ █████╗ ███████╗███████╗ │
│ / o o \ ██╔══██╗██╔══██╗██╔════╝██╔════╝ │
│ | ( ) | ██████╔╝███████║███████╗███████╗ │
│ \_====_/ ██╔═══╝ ██╔══██║╚════██║╚════██║ │
│ | | ██║ ██║ ██║███████║███████║ │
│ [____] ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝ │
│ │
│ 🔐 pass [?] help │
│ │
│ ▸ Email/ │
│ › gmail │
│ work │
│ Finance/ │
│ bank │
└─────────────────────────────────────────────────────────┘
↑↓ nav · enter open · n new · g gen · c copy · d del · / search · q quit
TUI keyboard shortcuts:
| Key | Action |
|---|---|
↑ / k |
Move up |
↓ / j |
Move down |
Enter |
Open entry (detail view) |
n |
New password (insert form) |
g |
Generate password |
c |
Copy password to clipboard |
d |
Delete entry (confirm prompt) |
r |
Rename entry |
/ |
Search / filter entries |
? |
Toggle keyboard help |
Esc |
Back / cancel |
q |
Quit |
Inside the detail view:
| Key | Action |
|---|---|
Space |
Reveal / hide the password |
c |
Copy password to clipboard |
e |
Edit in $EDITOR |
d |
Delete this entry |
Esc |
Back to list |
Error messages
| Message | Cause |
|---|---|
Error: store not found — run 'pass init' |
Store directory does not exist |
Error: identity file not found at ~/.age-identity |
Private key file is missing |
Error: wrong passphrase |
Incorrect passphrase entered |
Error: '<name>' not found |
Entry does not exist |
Error: '<name>' already exists. Use --force to overwrite |
Entry exists and --force was not passed |
Error: '<name>' is a directory; use -r to remove recursively |
Tried to rm a folder without -r |
Error: passphrases do not match |
The two passphrases entered during init differ |
Error: passwords do not match |
The two passwords entered during insert differ |
Directory structure reference
~\
├── .age-identity ← your encrypted private key (KEEP THIS SAFE)
└── .password-store\
├── .age-recipients ← your public key (safe to expose)
├── .git\ ← git history (if initialized with --git)
├── Email\
│ ├── gmail.age ← encrypted entry
│ └── work.age
└── Finance\
└── bank.age
Security considerations
What is protected:
The content of every .age file is encrypted and unreadable without your
passphrase and private key. An attacker who obtains only the .password-store
folder learns nothing about your passwords.
What is NOT protected:
- Entry names —
Email/gmail.age,Finance/bank.agereveal the existence and names of accounts. Visible in git history and to anyone with filesystem access. Use abstract names if this is a concern. - Passphrase in memory — while the terminal session is open and the key is
unlocked, the private key exists in process memory. Run
pass lockwhen stepping away. - Clipboard — when using
--clip, the password sits in the clipboard for up to 45 seconds. Other processes running as the same Windows user can read the clipboard. - Temp files —
pass editwrites a plaintext temp file during editing. It is deleted immediately after the editor closes, but it may persist in filesystem journals or undeleted pages.
Protecting your identity file:
# Restrict ~/.age-identity to your user account only
icacls "$env:USERPROFILE\.age-identity" /inheritance:r /grant:r "${env:USERNAME}:(R,W)"
Tips and tricks
Shell completion — generate tab-completion for PowerShell:
pass completion powershell | Out-File -Append $PROFILE
Quick copy workflow:
pass show Email/gmail -c # copies password, auto-clears in 45s
Batch search:
pass find "" # lists every single entry (empty pattern matches all)
pass grep "@" # find every entry containing an @ (likely all email fields)
Organize with folders — use / to create a hierarchy:
pass insert Work/VPN/company-vpn
pass insert Work/SSH/server-prod
pass insert Personal/Banking/savings
Export a single entry to clipboard for scripting:
$pw = pass show Email/gmail -f username
Write-Host "Logging in as: $pw"
Building from source
# Requirements: Go 1.21+
go build -o pass.exe .
# Cross-compile from Linux/macOS
$env:GOOS = "windows"; $env:GOARCH = "amd64"
go build -o pass.exe .
License
MIT
Documentation
¶
There is no documentation for this package.