# deja: fuzzy, directory-aware ghost text for zsh

> deja replaces zsh-autosuggestions with a Go daemon that ranks commands by fuzzy match, working directory and sequence prediction, storing everything in a local SQLite file. Here is how it installs, what the daemon actually does, and where it stops being the right tool.

**Giammarco-Ferranti/deja** — Predictive inline shell autosuggestions for zsh - Go daemon, no TUI, no sync.

- Repository: https://github.com/Giammarco-Ferranti/deja
- Stars: 769 · Forks: 15
- Language: Go
- License: MIT
- Published: 2026-08-04 · Updated: 2026-08-18 · Language: en
- Canonical page: https://hysenlabs.com/projects/giammarco-ferranti-deja

## What deja changes about zsh history suggestions

The default zsh autosuggestion story is prefix matching over history. You type `git`, and you get back the most recent command in ~/.zsh_history that starts with those three characters. That works until it does not: you type `gt` and get nothing, you are in ~/projects/foo but get a command you last ran in ~/work/bar, and you type `make` expecting `make test` because you always run `make test` after `make build`, and get whatever `make` invocation happened to be most recent.

deja is aimed at that gap. The README describes it as "a smarter replacement for zsh-autosuggestions" and lists four ranking inputs: fuzzy matching, directory awareness, sequence prediction and frecency scoring that blends frequency with recency under a one-week exponential decay. The audience is narrow and specific: zsh users who keep a long history, work across several project directories, and want the suggestion to reflect where they are rather than only what they typed. It is not a shell, not a prompt, and not a history search UI. It draws ghost text inline using zsh's POSTDISPLAY widget, so the suggestion sits at the cursor in the same line you are editing.

The design constraint the project states most clearly is what it refuses to do. No account, no sync server, no TUI. Everything lives in a local SQLite database. For anyone who has looked at a hosted command-completion product and wondered where the telemetry goes, that is the whole pitch in one sentence.

## The daemon, the SQLite file and the per-keystroke path

The architecture is a background process plus a thin zsh integration. The repository layout shows cmd/ for the binary entry point, internal/ for the implementation, and sqlc.yaml alongside a generated-code step in the Makefile, which tells you the query layer is written as SQL and compiled into Go rather than hand-rolled. The two runtime dependencies in go.mod are github.com/mattn/go-sqlite3 and github.com/sahilm/fuzzy, which matches the feature list exactly: one for storage, one for the matching.

On the shell side, `deja init zsh` does not print the integration script to stdout. It writes the script to ~/.local/share/deja/init.zsh and prints a `source` line pointing at that file. That distinction matters for startup cost. The README states that `eval "$(deja init zsh)"` spends a full binary launch, roughly 25 to 36 ms, on every shell you open, regenerating a file that is almost always byte-identical. Sourcing the file directly skips the launch.

The project also describes how the cached script stays current without that cost. Each shell compares the installed binary's stat identity against the identity baked into the script, a check the README puts at 0.083 ms with no subprocess. If they differ, the script is regenerated in the background. The shell that noticed the change continues with the old script, and the next shell picks up the new one. That is a deliberate trade: an upgrade never blocks a shell startup, but it also means one session can run against the previous integration for its lifetime.

The daemon itself is auto-spawned on first use and kept running across sessions, so multiple terminal windows share one process. The README claims under 1 ms response per keystroke. That figure is the project's own and I have not measured it; treat it as a design target rather than a verified number.

## Installing deja and getting the first suggestion

Homebrew is the shortest path on macOS and Linux. The README gives a single chained command that installs the formula, imports your history, appends the activation block to ~/.zshrc if it is not already there, and reloads the shell.

```bash
brew install Giammarco-Ferranti/deja/deja && deja import && (grep -qF 'deja/init.zsh' ~/.zshrc 2>/dev/null || echo 'if [[ -r "$HOME/.local/share/deja/init.zsh" ]]; then source "$HOME/.local/share/deja/init.zsh"; else eval "$(deja init zsh)"; fi' >> ~/.zshrc) && exec zsh
```

The grep guard is what makes it idempotent, so running it twice will not double-source the integration. Without Homebrew, the curl installer does the same work:

```bash
curl -fsSL https://raw.githubusercontent.com/Giammarco-Ferranti/deja/main/install.sh | sh
```

The README points at install.sh on GitHub if you want to read it before piping it to a shell, which is the right habit for any curl-to-sh installer.

If you already have the binary on your PATH and skipped the activation step, the two commands are import and init. `deja import` reads your history from $HISTFILE when it is exported, and falls back to ~/.zsh_history.

```bash
deja import
eval "$(deja init zsh)"
```

If your history lives elsewhere, or $HISTFILE is set in ~/.zshrc without export so child processes cannot see it, point deja at the file directly:

```bash
deja import --file /path/to/history
```

For a permanent setup the README recommends sourcing the generated file rather than evaluating init on every shell:

```zsh
# ~/.zshrc
if [[ -r "$HOME/.local/share/deja/init.zsh" ]]; then
  source "$HOME/.local/share/deja/init.zsh"
else
  eval "$(deja init zsh)"
fi
```

After that, open a new terminal, type a few characters of a command you have run before, and the ghost text should appear at the cursor. The README is explicit that you accept it with the right arrow, not Enter. Enter executes what is literally in your buffer. Other bindings from the README's table: Ctrl+right arrow accepts the next word only, Tab opens the inline alternatives picker, Shift+right arrow cycles the fuzzy preset forward through tight, smart and loose, Ctrl+X suppresses the current suggestion session-wide, and Shift+up arrow toggles ghost text on an empty prompt.

Oh My Zsh and zinit users have their own paths. The README warns to pick one activation, not both, because the plugin loads the integration for you and keeping the installer's appended lines as well double-sources it.

## Where deja gets in your way

The clearest limitation is the platform. The README documents macOS and Linux installs only, through Homebrew or a shell script, and the integration is a zsh script. There is no Windows path and no bash, fish or PowerShell support described. If your team is not on zsh, this is not a tool you can standardise on.

The second is the daemon. One background process serving all terminal windows is efficient, and it is also a process that has to be running for suggestions to appear. The README states deja auto-spawns it on first use and keeps it across sessions, but it does not document what happens when the daemon dies mid-session, whether the shell restarts it, or how to stop it deliberately. That is a real gap: a user debugging a frozen prompt has no documented command to inspect or restart the daemon.

Third, the interaction with zsh-autosuggestions is handled by standing down rather than merging. The README says that if deja detects zsh-autosuggestions is loaded, it stands down. That is a sensible safety choice, but it means a half-migrated config silently produces no suggestions at all, with the troubleshooting section as the only pointer. The README also says plainly not to list both in plugins=().

Fourth, the history-respect behaviour cuts both ways. Deja honours HIST_IGNORE_SPACE and HISTORY_IGNORE, so a command with a leading space stays out of deja as well as out of ~/.zsh_history. Good for secrets typed at the prompt. It also means that if you have been relying on a leading space to keep exploratory commands out of history, those commands will never become suggestions either, which may or may not be what you want.

Finally, the alternatives picker is a Tab-bound inline cycle, not a browsable interface. If you want to see your command store, edit entries, or delete something, the README does not describe a way to do that. The project's stated position is no TUI. That is a design decision, not an oversight, but it is a decision with a cost.

## deja against zsh-autosuggestions and fzf-style history search

The obvious comparison is zsh-autosuggestions, which deja explicitly positions itself against. The difference is the matching model. zsh-autosuggestions surfaces history entries that start with what you have typed, which is fast and predictable and requires no daemon. deja adds fuzzy matching, so skipped letters and reordered characters still match, and it adds two ranking signals that plain history lookup does not have: the directory you are in, and the command you ran immediately before. The README's example is that it "knows that you usually run `make test` after `make build`".

That extra signal is the whole reason the daemon exists. Prefix matching over a history file can be done in the shell. Scoring candidates by frecency with a one-week decay, weighting by current directory, and consulting a sequence model needs a persistent process and a queryable store, which is why deja ships SQLite and a background binary. You are trading a self-contained shell script for a process and a database file.

A different kind of alternative is fzf-style history search, where you press a key, get a fuzzy-filtered list of past commands, and pick one. That is retrieval on demand rather than prediction as you type. It never guesses wrong because it never guesses. If your problem is that you cannot remember a command you ran last week, interactive search is the better fit. If your problem is that you know what you want to type and want the buffer filled for you, deja's model applies. The two are not mutually exclusive, and the README does not claim otherwise.

## Maintenance, releases and the MIT licence

The repository is not archived. The last push was on 2026-08-07, the same day as the v0.4.1 release, so the code was being touched within the last two months. The release cadence visible in the release list is v0.3.2 on 2026-06-11, v0.4.0 on 2026-08-01, and v0.4.1 on 2026-08-07. That is a project still moving, with a patch release following a minor within a week.

The release machinery is conventional and visible in the repository root: .goreleaser.yaml for cross-platform builds, release-please-config.json and .release-please-manifest.json for automated versioning, and a CHANGELOG.md. The Makefile separates a stripped release build from an unstripped debug build, and its comments give concrete numbers for the difference: 12.4 MB with the symbol table and DWARF retained against 7.5 MB that ships, with -trimpath keeping local absolute paths out of the binary. It also carries a sqlc-verify target that runs `sqlc diff` to fail when the committed generated code is stale, with a comment noting it is worth wiring into CI because a forgotten `make sqlc` is otherwise invisible. That is the kind of detail that tells you the maintainer has been bitten by it.

Upgrade cost is unusually low by design. Because the shell compares the binary's stat identity against the one recorded in the cached script and regenerates in the background, an upgrade does not add a launch to shell startup. The trade is that the session that notices the change keeps running the old integration until you open a new shell.

The licence is MIT, which is permissive and places few obligations on how you redistribute or modify the code. I am not a lawyer and this is not legal advice; if you are vendoring deja into a product, read the LICENSE file in the repository rather than this paragraph. Worth noting for compliance purposes: the binary links github.com/mattn/go-sqlite3, which is a cgo binding to SQLite, so the SQLite public-domain notice and the cgo toolchain requirement travel with any build you make from source.

## Conclusion

Adopt deja if you live in zsh, dislike sync services, and want suggestions that account for the directory you are standing in and the command you just ran. Skip it if you use fish, bash or PowerShell, if you want a TUI to browse and edit your command store, or if you need a Windows build. Before committing, check that your $HISTFILE is exported or plan to pass --file, confirm nothing in your plugin list already loads zsh-autosuggestions, and read install.sh before piping it into sh.

## FAQ

### Does deja replace zsh-autosuggestions, or can I run both?

The README says deja replaces zsh-autosuggestions and that you should not list both in plugins=(). If deja detects zsh-autosuggestions is loaded it stands down, so running both leaves you without deja's suggestions.

### How do I accept a deja ghost suggestion?

Press the right arrow, not Enter. The README notes that Enter executes whatever is literally in your buffer, while the right arrow accepts the full suggestion. Ctrl+right arrow accepts only the next word.

### Where does deja keep my command history?

In a local SQLite database, according to the README, which describes the tool as local-only with nothing leaving your machine. There is no account and no sync server.

## Sources

- [Official README](https://github.com/Giammarco-Ferranti/deja#readme)
- [Project repository](https://github.com/Giammarco-Ferranti/deja)
- [Release notes](https://github.com/Giammarco-Ferranti/deja/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/giammarco-ferranti-deja
