Records shell commands into SQLite and searches them.
just install # release build, then into $BIN_DIR (default ~/bin)
just --list # all recipesRecipes run with this directory as the working directory. The SQLite build flags
live in .cargo/config.toml, and cargo finds them by walking up from the current
directory, so a cargo build invoked from elsewhere produces a differently
configured binary.
Path resolution order:
--db PATH$RECALL_DB$XDG_DATA_HOME/recall/history.db$HOME/.local/share/recall/history.db
--db is a global flag, valid before or within any subcommand. An unset or empty
XDG_DATA_HOME falls through to HOME; with neither set, the command fails.
Missing parent directories are created when the database is opened for writing.
recall path prints the resolved location.
This tool was previously called cli-history, and read $CLI_HISTORY_DB,
$CLI_HISTORY_SESSION and .local/share/cli-history/. Nothing falls back to
those names; an existing database moves once, WAL sidecars included:
data="${XDG_DATA_HOME:-$HOME/.local/share}"
mkdir -p "$data/recall"
for f in "$data"/cli-history/history.db*; do
[ -e "$f" ] && mv "$f" "$data/recall/${f##*/}"
done
rmdir "$data/cli-history"Connections use WAL journaling, synchronous = NORMAL, and a five second busy
timeout.
Schema (user_version = 3), table commands:
| column | type | null | notes |
|---|---|---|---|
id |
INTEGER | no | primary key, insertion order |
ts_ms |
INTEGER | no | Unix epoch milliseconds, UTC |
session |
TEXT | yes | from --session or $RECALL_SESSION |
host |
TEXT | yes | from --host or $HOSTNAME |
cwd |
TEXT | no | directory the command ran in |
exit_code |
INTEGER | yes | null when the caller reported none |
duration_ms |
INTEGER | yes | wall clock runtime, null when unmeasured |
command |
TEXT | no | the command line as bash recorded it |
Indexes: commands(ts_ms), commands(cwd), commands(session, id).
Migrations run when the database is opened for writing. v1 gains duration_ms
via ALTER TABLE; v2 to v3 rebuilds the table to drop NOT NULL from
exit_code and recreates the indexes, in one transaction. Rows written before an
upgrade hold nulls in the added columns.
A null exit_code prints as - in human output and as an empty field in
tsv. It is matched by neither --exit-code N nor --failed, because SQL
comparisons against null are never true; recall sql "SELECT ... WHERE exit_code IS NULL" reaches those rows.
Records one command. The command text comes from trailing arguments, or from stdin when none are given.
printf '%s' "$cmd" | recall log --exit-code "$?" --cwd "$PWD" --duration-ms 251
recall log --exit-code 0 -- git status
recall log -- something-whose-status-is-unknown| flag | default |
|---|---|
--exit-code |
null |
--cwd |
current directory |
--session |
$RECALL_SESSION, else null |
--host |
$HOSTNAME, else null |
--duration-ms |
null |
Blank or whitespace-only --session and --host values are stored as null. An
empty command text is a no-op.
A command is dropped when it repeats the previous command (by id) of the same
session in the same directory.
--duration-ms can be measured in bash without forking: stamp
$EPOCHREALTIME (bash 5+) in PS0, which is expanded after a command line is
read and before it runs, then subtract in PROMPT_COMMAND. Lines that ran no
command get no duration.
Prints recorded commands, ordered by ts_ms descending.
recall search # 50 most recent
recall search cargo --since 7d # substring match, last week
recall search --cwd . --limit 200 # one directory
recall search --failed --since 12h # non-zero exits
recall search --exit-code 130 # interrupted commands
recall search --succeeded --unique # deduplicated, exit 0 only
recall search --format tsv # tab-delimited fieldsFilters: positional substring query, --cwd (. resolves to the current
directory, all values canonicalized), --session, --since, and one of
--exit-code N, --succeeded (--exit-code 0) or --failed (non-zero). The
three exit filters conflict with each other. --limit defaults to 50; --all
removes the limit and conflicts with --limit.
--format:
| value | output |
|---|---|
human |
timestamp exit duration cwd command, default |
tsv |
timestamp \t cwd \t exit_code \t command |
command |
the command line only |
--unique collapses repeated command lines to their most recent occurrence,
after the other filters. --null (-0) separates records with NUL instead of
newline, for fzf --read0.
A command killed by a signal records the shell's status, so Ctrl-C appears as
--exit-code 130 and is also matched by --failed.
An fzf picker over the current directory's successful commands:
recall search --format command --unique --null --succeeded --cwd . \
| fzf --read0 +m--since accepts relative offsets 45s, 30m, 12h, 7d, 2w, or absolute
2025-12-01 and 2025-12-01T09:30:00. Absolute values are read in the local
time zone.
Durations print as 251ms, 1.25s, 3m04s, 2h07m.
Runs one statement and prints tab-separated rows, streaming, so | head stops
early. The statement can come from an argument or stdin.
recall sql --header "SELECT command, count(*) AS n, avg(duration_ms) AS avg_ms
FROM commands GROUP BY command ORDER BY n DESC LIMIT 20"
echo 'SELECT count(*) FROM commands' | recall sql
recall sql --write "DELETE FROM commands WHERE command LIKE 'kubectl%'"The database is opened read-only unless --write is given, in which case it is
opened through the normal path and migrations run first. --header prints column
names as the first row. NULL renders as an empty field, blobs as x'..'. A SQL
error exits non-zero with the SQLite message in the cause chain.
Working examples:
- prompt.sh
calls
logfromPROMPT_COMMANDand measures--duration-msinPS0. - functions.sh
wires
searchinto theCtrl-E,Ctrl-FandCtrl-Gfzf pickers.
just check # fmt --check, clippy -D warnings, tests
just test-one <name> # one test, with output
just compile-options # SQLite options the bundled amalgamation was built with
just db-pathRelease profile: opt-level = 3, fat LTO, one codegen unit, panic = "abort",
strip.
.cargo/config.toml removes SQLite subsystems this tool does not call (FTS3,
FTS5, R-tree, extension loading, the deprecated API, UTF-16, SOUNDEX) and sets
SQLITE_UNTESTABLE. Nothing there caps row counts, table sizes, expression depth
or query complexity, and SQLITE_ENABLE_STAT4 remains enabled.
Cross-compiling and publishing binaries: RELEASING.md.