simple multi-computer shell history backed by git
  • Go 98.8%
  • Just 1.2%
Find a file
Viktor Varland cf6558c335
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
docs: changelog for v2.5.0
2026-08-18 09:11:16 +02:00
.just feat: install to the XDG bin dir 2026-08-01 23:42:51 +02:00
.woodpecker ci: update to wrangle owned 2026-08-17 22:44:48 +02:00
internal feat: honour histignorespace 2026-08-18 09:10:09 +02:00
.gitignore chore: ignore local agent settings 2026-08-01 23:55:36 +02:00
CHANGELOG.md docs: changelog for v2.5.0 2026-08-18 09:11:16 +02:00
go.mod refactor: take shell quoting from gout 2026-08-18 08:22:38 +02:00
go.sum refactor: take shell quoting from gout 2026-08-18 08:22:38 +02:00
justfile refactor!: retire the shell scripts the binary now replaces 2026-08-17 15:50:36 +02:00
main.go fix: carry an explicit --db into the hook 2026-08-17 23:01:15 +02:00
README.md feat: honour histignorespace 2026-08-18 09:10:09 +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.

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 <1ms speed of a kernel level >> for appending a command to a file. I've accepted the trade-off of verbatim taking ~2ms to add a record to SQLite.

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.