How it works, and how it breaks

August 10, 2026 · View on GitHub

Verified 2026-08-10 against zoxide 0.9.8 on Linux, bash 5.3.

The mechanism

zoxide keeps a database of directory paths with a frecency score, and z foo picks the highest-scoring path whose text matches your keywords. It matches strings, not filesystem identity — which means two paths to the same directory are two independent entries, and both work.

So an alias is just a symlink that gets added to the database:

~/.local/share/zalias/deploy -> ~/work/infra/terraform-prod-eu-west-1

zoxide add ~/.local/share/zalias/deploy stores that path as written. z deploy matches it on the substring deploy and cds there. The kernel resolves the symlink on every subsequent file access, so you are, for all practical purposes, in the real directory.

Why it survives zoxide add without being rewritten

zoxide has a _ZO_RESOLVE_SYMLINKS setting, default 0. At 0, zoxide add records the path it was handed. At 1, it calls the equivalent of realpath first — and every alias immediately collapses onto the directory it points at, becoming a duplicate of an entry that already exists.

This is the one configuration that breaks zalias outright, and it breaks it quietly: no error, the alias simply stops being a distinct thing and z deploy goes back to finding nothing. If you use zalias, leave that variable alone.

Matching rules worth knowing

zoxide requires every keyword to match the path in order, and the last keyword to match the final path component. That is why hyphenated names behave so well:

Alias nameAnswers
personal-sitez personal, z site, z personal site
deployz deploy, z dep

And why a name that is a substring of everything is a bad name: if your repos live under ~/repos, an alias called repos can never be a discriminating query, because every candidate path contains that string already.

Failure modes

A name that collides with a real directory

Aliases compete with real paths on frecency, not on precedence. If you alias mwp and a repo named Claude-MWP-Admin-plugin already exists, z mwp is a coin-toss decided by which you visited more recently.

There is no fix in the tool, and adding one would mean special-casing aliases in ranking — which would break the property that makes this work at all (zoxide doesn't know aliases exist). The right move is to pick a name that isn't already in some path, or to accept the ranking. Check before you commit to a name:

zoxide query my-intended-name

Nothing found means the name is free.

zoxide's database can be deleted at any time with no real loss — it rebuilds as you move around. Aliases don't, because nothing ever "moves around" to a path you haven't visited yet.

Two situations produce a symlink that exists on disk but not in the database: wiping the database, and restoring dotfiles onto a fresh machine. Both are fixed by zalias sync, which re-adds every live alias. Run it after either.

Score decay under ZALIAS_RESOLVE=1

With ZALIAS_RESOLVE=1, arriving at an alias immediately moves you to the real path — so zoxide's own hook records the real path, and the alias path is never recorded as visited again. Its score decays while the real one grows.

That matters because _ZO_MAXAGE budgets total score across the whole database and evicts entries to stay under it. An alias that is never re-scored can eventually be evicted, at which point a jump that worked for months stops working with no diagnostic.

__zalias_resolve prevents this by calling zoxide add on the alias path before resolving. It costs one database write per jump and keeps the alias alive. If you reimplement the resolve behaviour yourself, keep that ordering.

Dangling aliases

zoxide add refuses a path that isn't a directory, so an alias pointing at something that never existed can't enter the database. An alias whose target is deleted later stays in the database but can never win a match, since zoxide filters unavailable directories at query time. zalias sync reports these and skips them; zalias lists them with their broken target.

Why not a wrapper around z?

The obvious alternative design is to intercept z, check a lookup table, and cd directly on a hit. It was rejected:

  • It would have to be re-implemented for each shell, and kept in step with zoxide's own init script, which is generated and changes between versions.
  • A table lookup is exact-match. You'd lose fuzzy matching on alias names — z dep wouldn't find deploy — which is half the reason to want this instead of a plain shell alias.
  • Aliases wouldn't appear in zi, in the fzf picker, or in frecency ranking against real paths, because they wouldn't be in the database.

Putting the alias in the database instead means zoxide treats it as an ordinary directory and every existing feature applies to it for free. The cost is the pwd seam documented in the README, which is a much smaller price.