simple multi-computer shell history backed by git
  • Go 99%
  • Just 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Viktor Varland a0907dab49
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
docs: changelog for v2.6.1
2026-10-21 11:33:04 +02:00
.just feat: install to the XDG bin dir 2026-08-01 23:42:51 +02:00
.woodpecker ci: build for darwin 2026-10-21 11:30:10 +02:00
internal feat: declare that the hook has no inputs, so a shell may cache it 2026-10-20 20:23:29 +02:00
.gitignore chore: ignore local agent settings 2026-08-01 23:55:36 +02:00
CHANGELOG.md docs: changelog for v2.6.1 2026-10-21 11:33:04 +02:00
go.mod feat: declare that the hook has no inputs, so a shell may cache it 2026-10-20 20:23:29 +02:00
go.sum feat: declare that the hook has no inputs, so a shell may cache it 2026-10-20 20:23:29 +02:00
justfile refactor!: retire the shell scripts the binary now replaces 2026-08-17 15:50:36 +02:00
main.go feat: move spooled commands in whenever the database opens 2026-10-20 19:54:32 +02:00
main_test.go feat: record from the prompt into the spool, not through verbatim add 2026-10-20 19:54:32 +02:00
README.md feat: record from the prompt into the spool, not through verbatim add 2026-10-20 19:54:32 +02:00

verbatim

Shell history stored in SQLite, synced via git.

Install

cd $HOME/dev/vlv
git clone git.meatbag.se:varl/verbatim.git verbatim
mkdir histdb

cd verbatim
just install    # builds and copies verbatim to ~/.local/bin ($XDG_BIN_HOME)

Or build manually:

go build -o verbatim .
install -d ~/.local/bin
install verbatim ~/.local/bin/

Migrate from log files

verbatim import ~/dev/vlv/histdb/

zsh

~/.zshrc:

eval "$(verbatim hook zsh)"

That is the whole integration. It defines a precmd that records each command as it is run, and a verbatim-search widget bound to Ctrl-R that picks one back out and puts it on the line.

The precmd runs no process. It appends the command, with the time, host and directory, to a spool beside the database (history.db.spool) — about 10µs — and whichever verbatim command next opens the database moves the spool in first, in one transaction. Ctrl-R is one of those, so a command is searchable from the next prompt on. Writing a row from the prompt itself was 3–7ms on a real disk, after every command: each add was the last connection to the database, and SQLite syncs its WAL to disk when the last connection closes.

Nothing is installed beside the binary and nothing is sourced out of the clone, so the glue can't be older than the verbatim it calls — which is what used to go wrong across machines.

Ctrl-R is where the picker belongs — it is the same question zsh's own search asks, answered across every host instead of this shell. --bind is there for when a shell wants it elsewhere, or not at all:

eval "$(verbatim hook --bind '^X^R' zsh)"   # somewhere else
eval "$(verbatim hook --bind '' zsh)"       # nowhere; stock Ctrl-R stays

The widget is defined either way, so it stays one bindkey away — and verbatim pick runs it from the command line regardless.

The picker wants fzf; without it the hook records but binds nothing.

Not everything is recorded. Commands run as root are skipped, and so are the ones you keep out of zsh's own history with a leading space, where histignorespace is set — that one matters more here than it does locally, since this database is synced across hosts and a secret committed to it is harder to take back than a line in a file.

$VERBATIM_FZF_OPTS is how the picker looks — an array, read at each invocation, so it can be reassigned freely:

VERBATIM_FZF_OPTS=(--height=100% --layout=reverse)   # full screen, prompt on top
VERBATIM_FZF_OPTS=(--height=12)                      # a smaller window

It defaults to --height=40% --layout=default --no-sort, which is the same share of the screen moor's ^G takes. What it cannot change is how a row is built and read back — --delimiter, --with-nth, --nth, --tabstop, --expect, --prompt and the binds are the picker's, and are applied last. Matching is over the command alone: the dimmed context is there to be read, so a run count of ×25 does not answer to 25.

Usage

verbatim [--db PATH] add [--timestamp T] [--hostname H] [--dir D] -- COMMAND...
verbatim [--db PATH] search [--host H] [--dir D] [--after T] [--before T] [--limit N] [--group] [QUERY...]
verbatim [--db PATH] pick [QUERY...]
verbatim [--db PATH] sync [DIR]
verbatim [--db PATH] import DIR
verbatim [--db PATH] stats
verbatim hook [--bind KEY] zsh

add fills in the timestamp, hostname and working directory itself; the flags are for importers speaking for entries they did not witness.

In the picker: enter puts the command on the line (or prints it), ctrl-y copies it, ctrl-r switches what is being searched — this directory, or everything on every host.

The picker lists each distinct command once — the most recent time it was run, where it was run, and ×N for how often. A menu, not a log: 74k rows of history are 19k commands, and a third of the rows are ls, git status and cd ... search --group is the same view from the command line; search alone stays a log, in order, every occurrence.

Default --db: ~/.local/share/verbatim/history.db

Alternatives

Of course, there are many other shell history tools that are more mature, have more features, solve the same problem in a different way, etc:

Those are my favourites of the ones I considered before settling on the two legacy shell scripts (later ported to Go -- and expanded on) was the fact that I want to rely on git for syncing, and I don't want to rely on a single server.

And it's hard to beat the speed of a kernel level >> for appending a command to a file — so that is what the prompt does, and SQLite gets the spool in batches, off the prompt's path.

With Git I can choose to pull from a specific machine, or a centralised forge depending on what I feel like, so I use that for sync.