CLI tool
zsh-users/zsh-history-substring-search avatar
zsh-users/zsh-history-substring-search

zsh-history-substring-search: Fish-style history search for Zsh

🐠 ZSH port of Fish history search (up arrow)

3,112 stars175 forksShellLicense varies

At a glance

What is it?
A clean-room port of Fish's substring history search, bound to the arrow keys. It solves one problem well, but it is a script you must source and bind by hand, not a plugin manager feature.
Who is it for?
Adopt it if you already live in Zsh and want Fish's type-then-cycle history behaviour without switching shells; the script is small, the mechanism is documented, and the last push was on 2026-09-18. Skip it if you want a manager to handle key bindings for you, or if you expect it to search a history file that is not already loaded into your session.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What zsh-history-substring-search actually changes about the arrow keys

In stock Zsh, the up arrow walks back through your command history one entry at a time. If you typed `git` twenty commands ago and want it again, you press up repeatedly until you find it, or you fall back to `Ctrl-R` and its own incremental interface.

This project is a clean-room implementation of the Fish shell's history search, as the README describes it: you type any part of a previous command, then press a chosen key and cycle through the matching entries. The query is a substring, not a prefix, so `commit` finds `git commit -m "fix parser"` without you remembering where in the line it sat.

It is for people who already run Zsh and want that one behaviour without changing shells. It is not a history manager, not a fuzzy finder, and not a completion engine. The whole repository is four top-level entries: a README, a `.plugin.zsh` wrapper for plugin managers, and the main `zsh-history-substring-search.zsh` script.

How the search walks your history and why direction is relative

The mechanism is a ZLE widget pair. The script defines `history-substring-search-up` and `history-substring-search-down`, and you bind keys to them. When invoked, the up widget selects the nearest command that both contains your current query and is older than the command currently in the buffer; the down widget selects the nearest one that contains the query and is newer than the current buffer. That relative framing is what makes repeated presses feel like scrolling rather than re-searching.

The README notes that `^U` aborts a search. It also documents a multi-line case: if a match spans more than one line, you press the left arrow to move the cursor off the end of the command, and then up and down move the cursor between the lines of that command; once the cursor reaches the first or last line, pressing again performs another search.

Highlighting is controlled by `HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND`, whose default is bold white on magenta. The README points at the Character Highlighting section of the zshzle(1) man page for the values you can assign.

The script reads from the history already loaded in your interactive session. Nothing in the README describes it reading a history file directly, so whatever your shell has in memory is the search space.

Installing zsh-history-substring-search and binding the first keys

The README lists several routes. Homebrew is the shortest on macOS:

bash
brew install zsh-history-substring-search
echo 'source $(brew --prefix)/share/zsh-history-substring-search/zsh-history-substring-search.zsh' >> ~/.zshrc

That appends a source line to `~/.zshrc`. After a new shell, the widgets exist but no keys call them, so nothing changes yet.

With Oh-my-zsh, the README clones into the custom plugins directory and activates the plugin:

bash
git clone https://github.com/zsh-users/zsh-history-substring-search ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-history-substring-search
exec zsh

For a manual setup, source the script and bind keys. The README tells you to run `cat -v` in your terminal, press up and down, and read the codes it prints. If it prints `^[[A` and `^[[B`:

bash
source zsh-history-substring-search.zsh
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down

If those codes do not work, the README offers terminfo as a fallback:

bash
bindkey "$terminfo[kcuu1]" history-substring-search-up
bindkey "$terminfo[kcud1]" history-substring-search-down

You can also bind `^P` and `^N` in emacs mode, or `k` and `j` in vicmd mode. After binding, typing a fragment and pressing up should replace the buffer with a matching older command.

Where the setup breaks: key codes, load order and the antigen trap

The README is candid that `cat -v` sometimes reports the wrong key codes, and that some users find `[OA` and `[OB` work even when they were not the observed values. This is the most common failure mode, and it looks like the plugin is broken when in fact the binding points at codes your terminal never sends.

Load order is a second trap. The README states that if you use zsh-syntax-highlighting alongside this script, you must load zsh-syntax-highlighting before this one. Get that backwards and the interaction is undefined by the documentation.

The antigen instructions carry a warning the README makes explicit: the key binding configuration goes after `antigen apply`, not before, and the example includes `HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE=1` in that block.

It is also the wrong tool if you want fuzzy matching, ranking, or a full-screen picker. This searches substrings in your loaded history and cycles them in order. If you need to search across a very large history file, or match on non-contiguous characters, this is not that.

How it differs from fzf-style history pickers

The natural comparison is a fuzzy history picker such as the fzf history widget, which people search for alongside this project. The approach differs at the root. fzf takes over the terminal with an interactive list, ranks candidates by fuzzy match score, and hands the selection back to the prompt. This script never leaves the prompt: it rewrites the ZLE buffer in place, and the ordering is history order, older or newer relative to what you have typed.

That means no ranking and no preview pane, but also no context switch. You keep your cursor, your current line, and your shell state. For a command you half-remember, cycling two or three times is often faster than opening a picker at all.

A second comparison point is zsh-autosuggestions, which also appears in searches around this project. Autosuggestions predict forward from history as you type; this script searches backward through what you already ran. They address opposite directions of the same annoyance, and the README does not discuss running them together.

Maintenance, licence and the upgrade surface

The repository is not archived, and the last push was on 2026-09-18. The most recent release listed is v1.1.0 from 2023-07-28, following v1.0.2 in 2019 and v1.0.1 in 2017. So the release cadence is slow, while the branch has seen recent commits. Treat the release tag as a stable point and the branch as where fixes land.

The upgrade cost is low in the ordinary case. The script is one file, and the public surface is the two widget names plus the `HISTORY_SUBSTRING_SEARCH_*` variables. If you installed through Homebrew or a plugin manager, upgrading is that manager's job; if you cloned it, a pull is enough. The risk sits in the bindings, not the script: a terminal or terminfo change can invalidate the key codes you chose, and you would rediscover that with `cat -v`.

The licence is not stated in the repository information available, and the repository's licence file is not among the top-level entries listed. That is worth resolving before you vendor the script into a company dotfiles repo. Nothing here is legal advice; check the repository's own licence terms.

Editorial conclusion

Adopt it if you already live in Zsh and want Fish's type-then-cycle history behaviour without switching shells; the script is small, the mechanism is documented, and the last push was on 2026-09-18. Skip it if you want a manager to handle key bindings for you, or if you expect it to search a history file that is not already loaded into your session. Verify two things before you commit: that your terminal's reported key codes actually drive the bound functions, and that you source zsh-syntax-highlighting first if you use it, because the README states the order matters.

Frequently asked questions

How do I install zsh-history-substring-search?

The README gives several routes: Homebrew with a source line appended to ~/.zshrc, cloning into Oh-my-zsh's custom plugins directory, zplug, antigen, or Zinit. For a manual install you source zsh-history-substring-search.zsh and then bind keys to the widgets yourself.

How do I use zsh-history-substring-search?

Type any part of a previous command, then press the key you bound to history-substring-search-up to select the nearest matching command older than the current one, or history-substring-search-down for a newer one. Press Control and U together to abort the search.

How do I search through my zsh history?

This script searches the history already loaded in your interactive session: you type a fragment and cycle matches with the bound keys. The README does not describe it reading the history file directly, so the loaded session history is the search space.

Official sources

  1. Issues
  2. README
  3. Releases
  4. zsh-users/zsh-history-substring-search on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/zsh-users-zsh-history-substring-search.svg)](https://hysenlabs.com/projects/zsh-users-zsh-history-substring-search)