Zoxide: a Better Cd Command
Key topics
The Hacker News community discusses Zoxide, a 'better cd command', with many users praising its effectiveness in improving their command line workflow, while others share alternative solutions and concerns about its fuzzy matching feature.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
1h
Peak period
47
3-6h
Avg / period
17.8
Based on 160 loaded comments
Key moments
- 01Story posted
Sep 23, 2025 at 12:48 AM EDT
4 months ago
Step 01 - 02First comment
Sep 23, 2025 at 2:04 AM EDT
1h after posting
Step 02 - 03Peak activity
47 comments in 3-6h
Hottest window of the conversation
Step 03 - 04Latest activity
Sep 24, 2025 at 2:07 PM EDT
4 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
fzf and zoxide are probably my two most game changing cli tools. They make the terminal feel so good.
I haven’t used it in a while though because I switched from git to jj.
https://github.com/rupa/z
I’d never heard of it but it was the first search result for “z github”.
https://github.com/agkozak/zsh-z
It gives you this potentially constantly shifting set of shortcuts, essentially, and the problem is that means I have to constantly check I did get the result I wanted, and that I haven't accidentally gone to the wrong place. I found that more annoying to me than just using tab completions or history, which are much more predictable.
I can see how someone who has different workflows or environments might find it great though.
My biggest annoyance at the moment (and this may be me missing something), is that I have two directories: "thing" and "thing-api". I'm doing work in "thing" much more often than in the "thing-api", but whenever I run "z thing", it takes me to "thing-api" first, and I have to "z thing" again to get to where I wanted to go. It ends up being more effort than if I'd just tab-completed or history searched a plain cd command.
Run `zoxide query -ls thing` to see the scores, and `zoxide add thing -s AMOUNT` to increase the score.
However I agree z should ideally have some syntax like `thing$` to denote a full directory name instead.
> My biggest annoyance at the moment (and this may be me missing something), is that I have two directories: "thing" and "thing-api". I'm doing work in "thing" much more often than in the "thing-api", but whenever I run "z thing", it takes me to "thing-api" first, and I have to "z thing" again to get to where I wanted to go. It ends up being more effort than if I'd just tab-completed or history searched a plain cd command.
AFAIK the z command does take frequency into account (or was it most recent visit). However to avoid going into thing-api instead of thing I believe you just type thing/ i.e. At the slash and z will take you to thing (that obviously doesn't work with tab completion though).
I found that after some time I have gotten so used to z (which I aliases to cd) that I wouldn't want to live without it.
One added bonus of such file tree bookmarks via variables (over a similar `alias a='cd foo'`) is that if you get muscle-memory/active memory for those few abbreviations other use cases like `make -C $a` also work. I usually leave `$hb` & `$lb` set to `$HOME/bin` and `/usr/local/bin`, for example.
As per the comment I did this so that I could have "history substitution expressions" stored in variables, then Alt-E - look (& maybe edit) & ENTER.
The history syntax itself is very cryptic, derived from early 1980s BSD csh. As with most such crypticnesses, figuring it out once, storing it somewhere you can remember, and expanding it on demand is a not awful way to learn it.
But besides cryptic history directives you could also use it for the `$a<Alt-E>` above to "see before you do". You need an extra `$`, though, as written. It would be pretty easy to auto-add a `$` prefix in `my-expand`, though (with Alt-E just becoming a sort of different kind of TAB for expansion rather than completion).
I have this alternative in my zsh config:
Then I can just use ~c, ~d or ~dd or anything temporary I want to put thereI see at least two downsides: 1) now you have to remember to say `make -C ~c` { not `make -C $c` which I think most would find more natural } 2) the hash cannot be exported to inheriting subprocesses like a regular scalar $var.
Of course, 1) is kind of weak since you can also use "~var" most places. It's just not as familiar to many as $var.
One notable equivalency is that Zsh prompt escapes for $PS1 and friends like %~ would treat both the same - converting them to a "~c" inside your prompt.
So, in terms of "looking like what you type", that maybe makes ~c better. Maybe there is some setopt to make Zsh expand %~ as $c or $dd? Not sure. There are a lot of setopts.
Anyway, I actually use the exporting feature to non-Zsh subprocesses myself. So, I'm pretty locked-in to vars not just hash entries.
(PS: In Zsh `cd -<tab>` will show a pop up menu of all directories in the history stack.)
1. `setopt autopushd pushdsilent`: This is the key component, `autopushd` automatically adds directories to the directory stack (and `pushdsilent` option does it without echoing added directories). Without this option, there’s no directory stack to traverse (unless directories are first manually added to it with `pushd` [for completion, `popd` pops directories off the stack navigating back to those directories]). `autopushd` just calls `pushd` on the current directory after each `cd`.
2. To get a menu, Zsh completion also has to be loaded, e.g., `autoload -Uz compinit && compinit -C`
3. Finally `zstyle ':completion:*' menu select` is optional, but that highlights the selected match, which is a nicer UI in my opinion.
Those are the options I used to get this working with `zsh -f` (which starts Zsh with a default config).
May I (auto-)suggest:
https://github.com/zsh-users/zsh-autosuggestions
Have fun ;-)
Population doing `cd ~/**<tab>` takes way way longer than 5 seconds though.
But regardless of the speed up one could add to cd + fzf in this case, I will still be using zoxide:
and `zi` starts instantaneous if I need the dynamic filteringedit: layout
Also, re:
> the problem is that means I have to constantly check I did get the result I wanted, and that I haven't accidentally gone to the wrong place.
Is there a reason you don't add your current path to your prompt? I don't know how I'd work without that, never knowing which directory I'm in.
But my question is specifically about relative vs. absolute paths when recalling directory traversal from history. I'm still struggling to follow how you'd use Zsh history as a zoxide replacement without always using absolute paths.
And I do have my path in my prompt, I'm not talking about something that actually takes time, but more interrupts flow (for me, as I say, I get how for other people it'd work better).
The other issue is that it creates a separate "hop" which adds key strokes and cognitive load (i.e., I can't just jump directly to a subdirectory or related directory I first have to jump to a "junction" directory then to my destination).
In any event, I could see how that would be a reasonable approach in the absence zoxide, but those are the reasons I personally still prefer zoxide. (For the record, zoxide has some nice techniques for making a match more specific, e.g., `z foo bar` will hop to a dir containing `bar` only if it's in a subdirectory containing `foo`.
From the readme:
> The key feature of McFly is smart command prioritization powered by a small neural network that runs in real time. The goal is for the command you want to run to always be one of the top suggestions.
> When suggesting a command, McFly takes into consideration:
- The directory where you ran the command. You're likely to run that command in the same directory in the future.
- What commands you typed before the command (e.g., the command's execution context).
- How often you run the command.
- When you last ran the command.
- If you've selected the command in McFly before.
- The command's historical exit status. You probably don't want to run old failed commands.
[1] https://github.com/cantino/mcfly
I love that mode in Atuin. I can never remember which of the run commands to use between make/cmake/bazel/yarn/npm/uv and hitting ctrl-r twice and scrolling up is better than having to root around in a readme, which I may or may not have bothered to write for my future self.
http://atuin.sh/
McFly sounds interesting! Added it to my list of things to investigate. Does it do multi-machine syncing?
I use emacs, so I have compilation buffers for those.
I used to have this fantasy that after I die, someone will care enough to go through my ~/projects folder and go through everything I worked on, and all those makefiles and readmes were going to help them, but no one cares that much for me. I'm okay with that, depressing as it is.
Yeah but the other commands I ran is so that one succeeds.
You can type `cd ~` and press CTRL+r to immediately fuzzy match commands you've run with `cd ~`. fzf naturally ranks paths to cd into on top. If you find that too noisy you can just hit CTRL+r with an empty prompt and then search for `^cd ~` to only find cd commands.
I've written about filtering related history with zsh here: https://nickjanetakis.com/blog/hooking-up-fzf-with-zsh-tab-c...
If you want to go into ultra lazy mode you can also type `cd ` and spam the up / down arrows to only show commands from your history where you cd'd into a directory. That use case is also covered in the above post. I normally don't use this for changing directories but it can be done.
The commands are written in rust, presumably because they're part of this warp shell advertised on readme?
I used to use this https://github.com/jodavaho/smartcd
'scd journal'
'scd logs personal'
Now I tab complete using fzf, but the above is what you want.
Bash is Turing complete. You "need" nothing else. You may want it for various reasons.
- Zoxide is probably sponsored by Warp, I doubt they have a relation beyond that, I'm not sure but they don't have much of a synergy beyond both being written in Rust. (The main point of relevance here is Warp is venture funded.)
- Zoxide having a database outside of the shell is actually a huge advantage to me, because it makes it easy to access your database outside of the shell (e.g., Zoxide integration in Vim).
If I have some long path like ~/src/open-source/dotfiles or /home/nick/src/open-source/dotfiles it all works with fuzzy matching. You can hit CTRL+r and then search for "cd dotfiles" and it finds it. The ~ isn't necessary, you can also do "^cd dotfiles"` for a tighter list of matches for paths that are more ambigious with other non-cd commands.
Just `find . -type d | fzf` to determine what dir to change to (or ~ for "anywhere else")
1. Make an alias fcd 2. Make a tab complete that does that for the command fcd
This is kind of 101 bash - just DIY.
Here's mine:
(2) is the hardest part - just write something that works with `complete` and fzf. Nowadays this is childs play for any AI to just spit out.
(1) is just a) set the new command b) make the completion call c) map that call to <TAB> completion. there you go.My point was that requiring a new shell (or even history) is a limiting factor here, and either backwards search over commands (as suggested ITT), or just plan fzf directory changes are more functional and already integrated into bash.
And I that brought up bash examples to show how it might be done? We've gone around in circles lads back on the bus.
I use autojump, which is a lot like zoxide (possible predates it). It stores all the directories you've visited in an SQLite DB (along with the rank for each). I wrote a shell keybinding that presents me with fzf, along with the directories I've visited, in rank order.
With just a few keystrokes, I can visit any directory I've ever visited, really fast. It doesn't need to be the top ranking directory for my query.
I can't live without it now.
For example I have a big ~/src dir where I keep all my code checkouts. If I type 'z src' intending to go to ~/src/foo/bar/src, will it be clever enough to realise that I am referring to the second instance of the string 'src'?
I currently use a Fish port of the original 'z'. It does ignore the common prefix of _all_ matches (so if I only ever used it within my ~/src tree, the problem would disappear) but after that binary exclusion it works exclusively on frecency.
~/src/linux/linux
~/src/linux/stable
And I want to go to the first one? It doesn't have a unique sequence of keywords in it :/
Unless it recognises that if I type "z lin lin" I want the one where it appears twice?
I'm beginning to think my directory structure might just be toxic haha
Some tools have a hard time solving some problems when the problem is us.
Could not imagine using regular cd for navigating file systems anymore.
[0]: https://news.ycombinator.com/item?id=45346715
something like
alias zg=‘zoxide —basedir $(git rev-parse --show-toplevel)’
https://github.com/ajeetdsouza/zoxide/pull/1027
Other nice tools I use: Fish for shell (https://fishshell.com/), Starship for prompt (https://starship.rs/), bat "a cat with wings" for file preview (https://github.com/sharkdp/bat).
When I write scripts I'll just target /bin/sh, or /bin/bash if necessary. Never saw a reason to write zsh or fish scripts.
I write most of my local-use scripts in fish now. Having access to all the little ergonomics like outputting colored text is just more pleasant.
Works great!
Am I missing something useful?
The folder-scoped and repo-scoped histories are great too.
It's really neat !
In any case, aberrations such as the excessive use of emojis and exaggeration are becoming increasingly common, which is yet another reason for me to distance myself from GitHub. For me, a README that more closely follows the conventions and minimalism of a classic man page is a sign of quality, and it could perhaps even be rendered in plain text to achieve a high signal-to-noise ratio.
My whole experience of it is just seeing it's advertisements on every CLI tool page. Someone else here probably has something more meaningful to say about it though.
Here's the command I ran on macOS:
It being a native binary instead of Python-based might also help it execute more instantaneously. Most Python-based CLI helpers that I tried add a slight but noticeable delay to simple commands, whereas zoxide is so quick it's easy to forget you even invoked a helper in the first place.
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1110899
Migrated to zoxide instead, seems to work fine! Only need to get used to using z instead of j, muscle memory hard to adjust, might set an alias :)
abbr --add c cd
And then the "most used paths" are just present in the history filtered by the prefix I already typed.
I am certainly there are a whole host of security reasons not to, but it sure would be handy if a parent process could easily just read the final state of all environmental variables of a child process and possibly integrate them back into its own.
Shells could just have a syntax for accepting sub process environmental variables. I'd propose something easy like starting a line with = absorbing all set environmental variables.
We could build a custom cd tool, "custom-cd-bin" in this example and all that would need to do is change the PWD variable.
Maybe this will be something for my dream shell I'm never going to actually get around to building. It would take something gross like wrapping setenv thoughIt's just rarely used beyond dotfiles because... Well.. it inherits all variables etc
https://docs.vultr.com/how-to-use-the-source-command-in-bash
If you want to make the transition explicit at the end of the script, you can do what the sister comment did, essentially
"source <(bash the-script|grep -Pom "xx inherited variables\n(.*?)\n yy inherited variables")"
I'm talking about inheriting the environmental state of any subprocess. You can't source a binary.
Basically somewhere that source would not behave as you wanted?
I'm also uncertain what you mean wrt a binary. A binary will only have the environment variables available to the shell it's executed in - or do you mean that you'd also want it to inherit variables the binary itself might read from disk via .env files or similar?
https://www.man7.org/linux/man-pages/man1/mkdir.1.html
Maybe that is why they love these CLI tools written by Rust. ;)
Learn your basic UNIX/POSIX utilities first, please, before you start preaching because of Rust. :D
Maybe, jus maybe the defaults for UNIX/POSIX was just not good and thats why they Rust based one?
Or maybe, just maybe you could make an alias for it, i.e. "alias mkdir="mkdir -p" so then it becomes the default?
sighs.
My current list of tools I install are:
- ripgrep. It’s just fast.
- fzf. For vim. I’ve never have a real use for it on the command.
- lf. Sometimes I want to quickly browse around a directory.
But more often than not, the core utilities work fine.
But yeah, personally I just configure the default tools and be done with it for almost life. :D
With the exception of the fuzzy part, which can be added separately, Bash already has facilities for all of those functionalities.
The "remembering" part can be done via dirs/pushd/popd. The "using the basename only" part can be done via CDPATH. The "automated" remembering could be done via a super simple function wrapper over 'cd', adding things onto the the dirstack or the CDPATH respectively (and with a selection menu, possibly fuzzy, when clashes occur)
Putting it all together to achieve the same effect feels like a very simple 10-liner bash function at best.
Not dissing the author's work or idea, nor is there anything wrong with reinventing the wheel in another language for fun; but if you're going to claim that "z is a better cd" because it bundles all that other functionality for things that are unrelated to cd, and for which there are already pretty decent unix-philosophy-adhereing commands that you could be using effectively already, then if you fail to mention these things in your documentation, it feels a bit ignorant / disingenuous. I would have preferred examples with "without z you'd have to use CDPATH for this, and dirstack for that, and maybe this 5-line wrapper for the menu thing, etc".
Otherwise it feels a bit like saying "grep is a better ed" or something.
What you could argue with is whether there's a point to replacing the cd built-in in order to include fizzy finding (personally I don't see the point of this either). But the reason all the other cd features are included is in order to make that viable.
It was quite cool to see the power of such a tool, until the day I wanted to ncd into a directory with some code experiments I had built and just delete them all. Unfortunately the completion sent me into another directory, which I noticed after deleting all my parents tax documents.
DOS 3.3 didn't have an undelete command like later versions of DOS, so a colleague of my dad had to spend an evening trying to restore some of that data with some external tools. On the positive side, he also installed DOS 6.22 (we were really behind).
I mean, it's a hammer. It's fine. You can hit things. And pull out nails.
This thing runs on batteries and needs syncing and it's a hammer. It didn't need improving. It's an improvement on a rock, and it's all improved out.
cd ~/foo; $COMMAND ; #optional cd ~ here
My history is full of this
34 more comments available on Hacker News