Published on 2026-08-28

fish-style autocd for PowerShell

In fish, .. is a command. So is ../.., ~, and ./src. fish tries to change directory when a command looks like one, without an explicit cd. Years of muscle memory. On Windows, in my PowerShell 7 setup, that memory hit a wall:

~
..
..: The term '..' is not recognized as a name of a cmdlet, function, script file, or executable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

the fish habit, in pwsh

I wanted .. to be a command in pwsh.

Turns out PowerShell has a hook for exactly this moment:

CommandNotFoundAction runs when command lookup fails, and the handler can hand back a script block to run instead. If the unknown command names a directory, Set-Location there. This goes into the profile:

# fish-style autocd: a directory typed as a command cd's into it.$ExecutionContext.InvokeCommand.CommandNotFoundAction = {    param($CommandName, $EventArgs)    # PowerShell retries a failed lookup as "get-<name>" first. Skip that pass.    if ($CommandName -like 'get-*') { return }    if (Test-Path -Path $CommandName -PathType Container) {        # the script block runs with no arguments, so capture the target in a closure        $target = $CommandName        $EventArgs.CommandScriptBlock = { Set-Location -Path $target }.GetNewClosure()        $EventArgs.StopSearch = $true    }}

Typed on the left, what it amounts to on the right:

$ ..     # cd ..$ ../..  # cd ../..$ ~      # cd ~$ src    # cd src

That is more generous than fish, which only reacts to names starting with ., / or ~. Here any existing directory works.

The get-* line is the part that cost me time. When a command is not found, PowerShell retries the lookup with get- prepended, so that Process runs Get-Process. It is in CommandDiscovery.cs:

if (!commandName.Contains('-') && !commandName.Contains('\\')){    discoveryTracer.WriteLine(        "The command [{0}] was not found, trying again with get- prepended",        commandName);

That inner lookup fails too, and it fires the hook first, with get-../... On Windows, get-../.. is a valid relative path: enter get-.., go up one. It normalizes to the current directory, Test-Path says yes, and ../.. becomes a no-op. Returning early on get-* lets the lookup fail through to the second call, the one with the real name.

The same snippet works unchanged in the Windows PowerShell 5.1 profile.