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 name | Answers |
|---|---|
personal-site | z personal, z site, z personal site |
deploy | z 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.
The database is disposable, the symlinks aren't
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 depwouldn't finddeploy— 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.