Featured image of post auto-venv: Fish Shell Auto-Activates Python Virtualenvs on cd

auto-venv: Fish Shell Auto-Activates Python Virtualenvs on cd

auto-venv is a Fish shell plugin that automatically activates and deactivates Python venvs on directory change. Supports .venv, venv, .env, env naming, uses git root for scope, and works with z, zoxide, and any other navigation tool.

Every time you cd into a Python project, you manually run source .venv/bin/activate.fish. Leaving means remembering to deactivate. Jumping with z is worse β€” the venv never activates at all. auto-venv handles all of it. Enter a directory and the venv activates. Leave and it deactivates.

Installation

With fisher:

1
fisher install nakulj/auto-venv

Or manually:

1
cp venv.fish ~/.config/fish/conf.d/venv.fish

Restart your terminal or run source ~/.config/fish/config.fish to apply.

How It Works

Watching PWD Instead of Overriding cd

This is the most important design decision. Most venv auto-activation tools override the cd builtin, which means tools like z, zoxide, and pushd bypass the hook entirely β€” the venv never activates.

auto-venv watches Fish’s special $PWD variable instead. Any directory change, regardless of how it happened, fires the handler:

1
2
3
4
function __auto_source_venv --on-variable PWD
  status --is-command-substitution; and return
  __handle_venv_activation (__venv_base)
end

The status --is-command-substitution; and return guard prevents it from firing inside $(...) subshells.

Using the Git Root as the Lookup Base

1
2
3
function __venv_base
  git rev-parse --show-toplevel 2>/dev/null; or pwd
end

Inside a git repository, the venv is always looked up at the repo root β€” not the current subdirectory. So navigating from myproject/ into myproject/src/utils/ keeps the venv active. Outside a git repo, it falls back to pwd.

Recognizing Four venv Names

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function __venv --argument-names dir
  set VENV_DIR_NAMES env .env venv .venv
  for venv_dir in $dir/$VENV_DIR_NAMES
    if test -e "$venv_dir/bin/activate.fish"
      echo "$venv_dir"
      return
    end
  end
  return 1
end

It searches env, .env, venv, .venv in that order, checking for the existence of bin/activate.fish. Returns the first match.

Activation and Deactivation Logic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function __handle_venv_activation --argument-names dir
  set -l venv_dir (__venv $dir); or begin
    # No venv found β€” deactivate the current one if there is one
    set -q VIRTUAL_ENV; and deactivate
    return
  end

  # Avoid re-activating the same venv
  if test "$VIRTUAL_ENV" != "$venv_dir"
    source "$venv_dir/bin/activate.fish"
  end
end

Three cases:

  1. Venv found, different from current β†’ activate it (activate.fish deactivates the old one internally)
  2. Venv found, same as current β†’ do nothing (navigating within the same project doesn’t re-source)
  3. No venv found β†’ deactivate if one is active

In Practice

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Enter a project with .venv β€” auto-activates
cd ~/projects/my-api
# (my-api) ← venv name in prompt

# Navigate into a subdirectory β€” venv stays
cd src/controllers
# (my-api) ← still active

# Switch to another project with its own venv β€” auto-switches
cd ~/projects/ml-project
# (ml-project) ← switched

# Leave to a directory with no venv β€” auto-deactivates
cd ~
# ← prompt back to normal, venv deactivated

# Works with z too
z my-api
# (my-api) ← auto-activated

Configuration

auto-venv has no configuration. The venv directory names are hardcoded (env, .env, venv, .venv). If your project uses a different name (e.g., .python-env), fork the repo and edit the VENV_DIR_NAMES list in __venv.

Limitations

  • Only supports standard Python venvs (python -m venv, virtualenv) β€” detection relies on bin/activate.fish existing
  • conda environments are not supported β€” conda has its own activation mechanism
  • pyenv-virtualenv is not supported for the same reason

Manual vs auto-venv

ScenarioManualauto-venv
cd into projectMust source activate.fishAuto-activates
Navigate subdirectoriesVenv stays (no deactivate)Venv stays
cd out of projectMust remember to deactivateAuto-deactivates
Jump with z/zoxideVenv doesn’t activateAuto-activates
Switch between projectsDeactivate then activateAuto-switches

Summary

The entire plugin is under 40 lines. Watching $PWD instead of overriding cd makes it compatible with every navigation tool. Using the git root as the lookup base means moving between subdirectories never accidentally kills the venv. Once installed, you stop thinking about virtual environment management entirely.

References