Per-directory command history that powers fish-style inline autosuggestions in bash and zsh.
seasalt suggests the rest of your command while you type
(the shell integration — ble.sh or
zsh-autosuggestions —
displays the suggestion as gray ghost text; → accepts it), keeps history
scoped to the directory you are working in, and exposes a small CLI to
search and maintain that history. It is a single Rust binary plus
shell integration snippets, and it uses its own SQLite store —
it does not touch the shell's history file.
- Inline autosuggestions — seasalt suggests the most likely completion of the current line; the shell integration (ble.sh for bash, zsh-autosuggestions for zsh) renders it inline in gray.
- Per-directory history scoping — suggestions prefer history from the current directory, then parent directories (nearest first), then fall back to global history. Within each scope, the latest matching command wins, and commands matching the exact case are preferred over case-insensitive matches (like fish).
- Stale-file filtering — a command that referenced files which no
longer exist in the current directory is skipped as a suggestion
(the next candidate is tried instead). Only arguments that were
existing files when the command was recorded constrain matching, so
echo helloorgit pushare unaffected. - Duplicate suppression — re-running the same command in the same directory refreshes its existing entry (moving it to the top) instead of adding another copy, like fish.
- Space-prefix suppression — commands that start with a space or
tab are never recorded, like bash's
HISTCONTROL=ignorespace. Runpassword-commandwhen you do not want the command (or its arguments, e.g. a secret) to appear in history or suggestions. - Multi-line command support — multi-line commands (e.g. heredocs,
backslash continuations) are recorded verbatim and suggested by their
first line;
seasalt searchescapes embedded newlines, tabs, and backslashes so every entry stays on a single line. - Exit-code tracking — every recorded command stores its exit code.
- History size limit — history is automatically trimmed to the
newest 100,000 entries (configurable via
SEASALT_HISTORY_MAX,0disables trimming).seasalt cleardeletes everything and reclaims the file space. - Search and delete CLI —
seasalt searchqueries history across all directories or scoped to one, andseasalt delete ID...removes entries by id (e.g. a password recorded by accident).
- bash 4+ with ble.sh (0.4.0 development builds are fine) — required for autosuggestions.
- zsh >= 5.0.8 with the zsh-autosuggestions plugin — required for autosuggestions.
- A Rust toolchain to build from source (or Nix).
ble.sh must be sourced in .bashrc before the seasalt integration
snippet. Recording hooks also work with
bash-preexec as an
alternative to ble.sh, but suggestions require ble.sh.
For zsh, see the dedicated zsh section below.
curl -L https://github.com/miyakogi/seasalt/releases/latest/download/seasalt-x86_64-unknown-linux-musl.tar.gz | tar xz
sudo install -m 755 seasalt /usr/local/bin/seasaltThe release asset is a static binary for Linux x86_64 — no Rust toolchain or dependencies required.
git clone https://github.com/miyakogi/seasalt seasalt
cd seasalt
cargo install --path .nix build .#default
# result/bin/seasalt is the binaryAdd seasalt as a flake input and install the binary through
home.packages:
# home-manager flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager";
seasalt.url = "github:miyakogi/seasalt";
};
# home-manager module
home.packages = [ inputs.seasalt.packages.${pkgs.system}.default ];
}Pin a release tag for stability: seasalt.url = "github:miyakogi/seasalt/v0.2.3";. After installing, add
eval "$(seasalt init bash)" to ~/.bashrc as usual (see
Setup).
Add the following to ~/.bashrc, after the line that sources
ble.sh:
eval "$(seasalt init bash)"That is all — the snippet registers the preexec/precmd hooks for recording and the auto-complete source for suggestions, replacing the bash history and atuin inline-suggestion sources (details in Coexistence with atuin).
If seasalt is not on PATH (e.g. it lives in a Nix store path), set
SEASALT_BIN to the full path and use it to generate the snippet:
export SEASALT_BIN=/path/to/seasalt
eval "$("$SEASALT_BIN" init bash)"Add the following to ~/.zshrc:
eval "$(seasalt init zsh)"That is all — the snippet registers the preexec/precmd history hooks and a zsh-autosuggestions strategy that produces the inline suggestions.
Requirements — zsh >= 5.0.8 and the zsh-autosuggestions plugin.
Source zsh-autosuggestions before the seasalt init zsh line so the
custom strategy is available when zsh-autosuggestions is set up.
seasalt keeps itself first in ZSH_AUTOSUGGEST_STRATEGY, so its
per-directory suggestions take priority even when another tool (such as
atuin) later prepends its own strategy. Other strategies remain as
fallbacks, only running when seasalt has no match.
Inline suggestions come from seasalt's custom zsh-autosuggestions strategy; if zsh-autosuggestions isn't loaded, only history recording works (a warning is printed).
History is unified across bash and zsh — both shells share a single
database. Each record is tagged with the shell it came from, visible as
the last column of seasalt search --tsv.
The CLI is mainly driven by the shell integration, but every piece is usable by hand:
seasalt record --cwd DIR --session SESS -- CMD...
Record a command into history. Prints the row id. Re-running the
same command in the same directory updates the existing entry.
Commands starting with a space or tab are not recorded.
seasalt exit --last-id ID --code CODE
Store the exit code of a recorded command.
seasalt suggest --cwd DIR -- LINE...
Print the best history match for the given line, or nothing.
seasalt search [--cwd DIR] [--all] [--limit N] [--tsv] PATTERN
Search history. Default prints one "id<TAB>cmd" line per entry;
--tsv prints id, cwd, cmd, exit_code, started_at, shell separated by
tabs. By default search is scoped to the current directory; use
--all for everything. PATTERN is matched as a substring (SQL LIKE
semantics), so % and _ act as wildcards. Embedded backslashes,
newlines, and tabs in commands are escaped as `\\`, `\n`, and `\t`
so every entry stays on a single line.
seasalt delete ID...
Delete history entries by id. Multiple ids can be specified at
once, separated by spaces. Silently ignores ids that do not exist
and prints nothing on success.
seasalt clear
Delete all history entries and reclaim the file space (VACUUM).
Prints nothing on success.
seasalt init bash
Print the bash integration snippet.
seasalt init zsh
Print the zsh integration snippet.
Failures are silent: record, exit, and suggest never write to
stderr and exit non-zero instead, because they are called from shell
hooks.
With history like this (/tmp being an unrelated directory):
/proj/sub cargo build
/proj cargo check
/tmp cargo doc
running cargo in /proj/sub suggests cargo build; in
/proj or /proj/deep, cargo check; anywhere else, cargo doc.
Enter the development shell (pinned Rust toolchain: cargo, rustc, rustfmt, clippy, rust-analyzer):
nix developWith direnv, the repo ships a .envrc that
enters the same shell automatically:
direnv allowThe database lives at:
$SEASALT_DATA_DIR/history.sqlite3ifSEASALT_DATA_DIRis set (also used for testing),- otherwise
$XDG_DATA_HOME/seasalt/history.sqlite3, - otherwise
~/.local/share/seasalt/history.sqlite3.
The file is created automatically on first use. WAL mode is enabled for concurrent access by multiple shells.
Set SEASALT_PRIVATE_MODE to a non-empty value to stop recording
commands (like fish's $fish_private_mode); existing history and
suggestions are unaffected. Unset it to resume recording.
Set SEASALT_HISTORY_MAX to change the automatic history size limit
(default 100,000 entries; the oldest entries are dropped on record).
0 disables trimming. Unparsable values fall back to the default.
seasalt keeps an independent store and does not interfere with atuin:
- atuin — history search (
Ctrl-R), sync, stats. - seasalt — inline autosuggestions and per-directory scoping.
For inline suggestions, seasalt is the only source: the bash integration
snippet removes the atuin-history and bash history auto-complete
sources from _ble_complete_auto_source on the first idle, so inline
suggestions come only from seasalt. On zsh, seasalt keeps itself first in
ZSH_AUTOSUGGEST_STRATEGY, so its per-directory suggestions win even if
atuin later prepends its own global strategy; atuin (and other strategy
tools) remain as fallbacks only when seasalt has no match. atuin's own
history search (Ctrl-R) is unaffected.
atuin keeps its own history store and seasalt does not read or copy
from it: commands recorded by atuin do not appear in seasalt's
suggestions, and vice versa.
- Case-insensitive suggestion matching only folds ASCII case (SQLite's
LIKE/GLOB); non-ASCII case pairs such ascafé/CAFÉare treated as distinct. - Autosuggestions require ble.sh; without it only recording works (via bash-preexec) and a warning is printed on eval.
- Interactive
Ctrl-Rsearch is intentionally out of scope:seasaltkeeps search CLI-only; atuin covers interactive history search (see Coexistence with atuin). - Tab completion is ble.sh's standard completion; a fish-style completion database (per-command descriptions) is intentionally out of scope.
- No history sync across machines or users.