Your Shell History Is a Lie
You run Ctrl+R, type three characters, and bash cycles through the same five commands from last Tuesday. The thing you actually need — that docker run incantation with seventeen flags you pieced together at 2 AM — is gone. Overwritten. Lost to the HISTSIZE=1000 abyss.
Here’s what stock shell history gets wrong:
- One flat file.
~/.bash_historyor~/.zsh_history. That’s it. No metadata, no structure, no timestamps by default. - Overwritten on exit. Open two terminals, close them in the wrong order, one history silently nukes the other.
- Machine-local. Your work laptop doesn’t know what your home server ran last week.
- No context. Which directory? Which machine? Did it succeed? No idea.
The bash HISTSIZE and HISTFILESIZE settings exist, and yes, you can bump them to 100,000 and set HISTTIMEFORMAT to get timestamps, and enable HISTAPPEND so sessions don’t clobber each other. People do this. It works, technically, in the same way that a really large filing cabinet technically solves the problem of organizing documents. You still can’t search it properly, you still can’t sync it, and you’re still one history -c away from nuking everything.
Atuin fixes all of this at the root. It replaces your history file with a SQLite database, gives you a proper full-text search interface, and — optionally — syncs everything E2E-encrypted across every machine you own. It’s the shell history you deserved in 2006 but didn’t get until now.
Install It
One curl, done:
curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | shThat installs the atuin binary and drops the shell shim into your config. You’ll need to reload your shell after:
exec $SHELLShell integration is handled automatically by the installer for bash, zsh, and fish. If you’re doing it manually or something went sideways, add this to your rc file:
For zsh (~/.zshrc):
eval "$(atuin init zsh)"For bash (~/.bashrc):
eval "$(atuin init bash)"For fish (~/.config/fish/config.fish):
atuin init fish | sourceRestart your shell, run a few commands, then hit Ctrl+R. You’ll immediately see the difference — a full-screen fuzzy search overlay with timestamps, directories, and exit codes. Welcome to 2024.
Using It: More Than Just Ctrl+R
Atuin replaces Ctrl+R with its own TUI search. But it’s not just “fancier reverse search” — it’s a queryable database.
The Search Interface
Hit Ctrl+R. You get a filterable list showing:
- The command
- How long ago it ran
- Which host it ran on
- Which directory it ran from
- The exit code
Type to filter. Enter to execute. Tab to fill the prompt without executing (handy when you want to edit before running).
Filtering by Context
The real power is filtering. In the search box:
host:workstation docker runShows only docker run commands from your workstation. Combine filters:
dir:/etc/nginx exit:0Commands run from /etc/nginx that succeeded. This is the “which nginx reload actually worked?” query you’ve been doing mentally for years.
From the CLI directly:
# Show last 50 commandsatuin history list --limit 50
# Search by exit code (failures only)atuin history list --exit 1
# Commands from a specific hostatuin history list --host homelab
# Commands from a specific directoryatuin history list --cwd /var/wwwThe Stats Are Genuinely Fun
atuin statsSample output:
Total commands: 14,823Unique commands: 3,241Most used: git status (847) ls (712) cd .. (601) docker ps (487)You will immediately discover that you type git status more than you breathe. Humbling.
Config: Dotfile It
Atuin’s config lives at ~/.config/atuin/config.toml. Key knobs worth setting:
# Don't sync automatically — we'll use our own serverauto_sync = falsesync_frequency = "5m"
# Search UI behaviorsearch_mode = "fuzzy" # fuzzy | prefix | fulltextfilter_mode = "global" # global | host | session | directorystyle = "compact" # auto | compact | full
# Key bindingsctrl_r_search_mode = "workspace" # search current dir by default
# Don't record thesehistory_filter = [ "^secret", "^export.*TOKEN", "^export.*PASSWORD", "^export.*SECRET",]
# Don't capture commands with these exit codes in certain contexts# (keep failures for debugging, this is personal preference)The history_filter field is a regex list. Any command matching a pattern gets silently dropped from the database. Useful so you stop accidentally logging export AWS_SECRET_ACCESS_KEY=... into searchable history. Your future self will appreciate this.
Self-Host the Sync Server
This is where it gets good for home lab types. Atuin ships an official sync server (atuin-server) you can run yourself. Your history never touches Atuin’s cloud infrastructure — everything syncs through your own box, encrypted client-side before it leaves your machine.
The key point: the server never sees your history in plaintext. Encryption and decryption happen entirely on the client. If your server gets compromised, the attacker gets encrypted blobs. No key, no history.
Compose Setup
services: atuin: image: ghcr.io/atuinsh/atuin:latest restart: unless-stopped command: server start ports: - "8888:8888" environment: ATUIN_HOST: "0.0.0.0" ATUIN_PORT: "8888" ATUIN_OPEN_REGISTRATION: "false" # set true only when adding new users ATUIN_DB_URI: "postgresql://atuin:changeme@db/atuin" depends_on: db: condition: service_healthy volumes: - atuin-config:/config
db: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: atuin POSTGRES_PASSWORD: changeme POSTGRES_DB: atuin volumes: - atuin-db:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U atuin"] interval: 5s timeout: 5s retries: 5
volumes: atuin-config: atuin-db:Spin it up:
docker compose up -dRegister and Connect
On your first machine, register an account (this is local to your server — not Atuin’s cloud):
--server http://your-server:8888On every other machine, log in:
atuin login -u yourname -p 'strongpassword' \ --server http://your-server:8888Import your existing history so you don’t lose it:
atuin import autoThen do an initial sync:
atuin syncAfter that, syncing happens automatically based on sync_frequency in your config. Or trigger it manually any time:
atuin sync -f # force full syncHow It Compares: mcfly and hstr
You’ve probably heard of the other two players in this space.
hstr is the OG. It gives you a better Ctrl+R with color-coded frequency indicators. No sync, no database — just reads your existing history file with a nicer UI. If you want zero-setup improvement to history search and nothing else, hstr is fine. It’s the ls --color of shell history tools.
mcfly replaces history search with a neural network that learns your patterns and ranks suggestions by context (current directory, recent commands). Genuinely clever. The suggestions get better over time. But: no sync across machines, no stats, no filtering by exit code. And honestly, “neural network for shell history” is a bit of a forklift for what you’re actually doing.
Atuin is the most complete solution if you use more than one machine. The sync story is first-class, the filtering is powerful, and the self-hosting option is actually good — not an afterthought. The tradeoff is slightly more setup upfront.
| Feature | hstr | mcfly | Atuin |
|---|---|---|---|
| Better Ctrl+R UI | Yes | Yes | Yes |
| Timestamps + metadata | No | Partial | Yes |
| Exit code filtering | No | No | Yes |
| Multi-machine sync | No | No | Yes |
| Self-hosted server | N/A | N/A | Yes |
| E2E encryption | N/A | N/A | Yes |
| Stats | No | No | Yes |
A Note on Privacy
Atuin’s public sync service (sync.atuin.sh) uses the same E2E encryption as self-hosting. Your key never leaves your machine — the server stores ciphertext. This is documented and the crypto is open source. If you trust the implementation, the public service is fine.
If you don’t trust it — or you just prefer keeping your data on hardware you control — self-hosting takes about ten minutes with the Compose stack above. That’s the home lab way.
Either way, set history_filter for anything sensitive. Belt and suspenders.
Keybindings Worth Knowing
By default Atuin binds Ctrl+R for history search and Up Arrow for inline history navigation. You can remap both:
[keys]# Vim-style: Ctrl+H opens history search# ctrl_r = "ctrl-h"
# Disable the up-arrow binding if it conflicts with your existing setupup_arrow_select = falseThe up_arrow_select toggle is worth knowing. Some people love it — Up scrolls through history in Atuin’s TUI directly from the prompt. Others find it breaks their mental model of the up arrow. Personal preference. Turn it off if it annoys you.
There’s also Ctrl+O by default to switch between filter modes without opening config. Hit it while in the search TUI to cycle between global (all machines), host (current machine only), session (current terminal session), and directory (current working directory). Muscle memory for that one pays off fast.
Migrating Without Losing Anything
Before Atuin rewrites your shell init, it doesn’t delete your existing history file. The import command reads it and loads everything into the SQLite database:
# Detects your shell and imports automaticallyatuin import auto
# Or be explicitatuin import bash # reads ~/.bash_historyatuin import zsh # reads ~/.zsh_historyOld history comes in without timestamps — Atuin assigns the import time as a placeholder. That’s fine. You’re not losing the commands, you’re losing the original timestamps, which you probably didn’t have anyway because HISTTIMEFORMAT is off by default.
After import, verify the count:
atuin statsIf the total looks roughly right compared to your old history file, you’re good. Now you can set HISTFILE=/dev/null in your rc if you want to fully cut over, or just leave the old file in place — Atuin ignores it from here on.
The Bottom Line
If you use one machine and never lose history, Ctrl+R is probably fine. But the moment you’re bouncing between a laptop, a workstation, and a server — or you’ve lost that one perfect command for the third time — Atuin is the right call.
Install takes two minutes. Self-hosted sync with Postgres takes ten. You get encrypted, searchable, context-aware history across every machine you own, backed by a database instead of a text file that one errant > could obliterate.
Stop being a human cron job for your own command history. Let the database do its job.
Your 2 AM self will appreciate it.