Blog

It's minimal, but I'm posting things.

Your favorite dev tools shouldn't pollute a shared codebase and bother your coworkers.

Solution: Combining a version manager such as asdf with an environment variable manager such as direnv.

The Stack: asdf, direnv, and uv

I use asdf1 for runtime management.
It handles my Python versions through a simple .tool-versions file.

direnv2 loads environment variables when I enter a directory and wipes them when I leave.

For Python, I migrated my entire workflow to uv3. It is fast enough that I can sync dependencies every time I switch projects without noticing the latency.

The helpful provisioning script

I implemented a script called direnv-provision-python.sh (see end of article for implementation), and added it to my $PATH.
It parses pyproject.toml, extracts the exact version, and generates the necessary local configuration.

It also injects my personal tools, like pdbpp which is a Python debugger library I use when working in Vim.

layout python3 .venv
uv sync --active --preview-features extra-build-dependencies
uv pip install --quiet pdbpp bpython

So I get my debugger and my AI tools. My teammates get a clean project file.

Staying invisible with global ignores

The last piece of the puzzle is Git hygiene.
I don't want to be put in a situation where I must explain why .envrc or .tool-versions are showing up in the repository for my team.

So, what you can do, is specify in your global git configuration an exclusion file, in which you add glob exclusion patterns (see end of article for those).

[core]
  excludesfile = /home/dehi/.ignore-git

My ~/.ignore-git file hides my local artifacts from every repository on my machine. It keeps my workflow personal and my commits professional.

Thats it.

Now, you'll enjoy automatic runtime version selecting, environment variable provisionning, and all without polluting your collegues workspace.

direnv-provision-python.sh

#! /usr/bin/zsh
direnv-provision-python() {
  # Helper to provision a Python project with direnv.
  if [[ ! -d "$(pwd)" ]]; then
    echo "Error: Current directory does not exist or is inaccessible" >&2
    return 1
  fi
  if [[ ! -w "$(pwd)" ]]; then
    echo "Error: Current directory is not writable" >&2
    return 1
  fi
  
  # Extract Python version from pyproject.toml and create .tool-versions
  if [[ -f "pyproject.toml" ]]; then
    local python_version
    python_version=$(python3 -c "
import sys
try:
    import tomllib
except ImportError:
    import tomli as tomllib

try:
    with open('pyproject.toml', 'rb') as f:
        data = tomllib.load(f)
        requires = data.get('project', {}).get('requires-python', '')
        if requires and '==' in requires:
            print(requires.split('==')[1].strip())
        else:
            print('', file=sys.stderr)
            sys.exit(1)
except Exception as e:
    print(f'Error parsing pyproject.toml: {e}', file=sys.stderr)
    sys.exit(1)
" 2>/dev/null)
    
    if [[ -n "$python_version" ]]; then
      echo "python $python_version" > .tool-versions
      echo "Created .tool-versions with python $python_version"
    else
      echo "Warning: Could not extract Python version from pyproject.toml" >&2
    fi
  fi
  
  local start_marker="### DIR_ENV_PROVISION_PYTHON_START ###"
  local end_marker="### DIR_ENV_PROVISION_PYTHON_END ###"
  local deps_assignment
  if (( $# )); then
    deps_assignment="required_deps=("
    local dep
    for dep in "$@"; do
      deps_assignment+="\"${dep//\"/\\\"}\" "
    done
    deps_assignment="${deps_assignment% }"
    deps_assignment+=")"
  else
    deps_assignment="required_deps=()"
  fi
  local content
  content=$(
    {
      cat <<'EOF'
PATH_add "$HOME/.asdf/shims"
layout python3 .venv
uv sync --active --preview-features extra-build-dependencies
uv pip install --quiet pdbpp bpython
EOF
    }
  )
  local desired_block="$start_marker
$content
$end_marker"
  # Ensure .envrc exists before any operations
  [[ -f .envrc ]] || touch .envrc
  local existing_block=""
  existing_block="$(sed -n "/$start_marker/,/$end_marker/p" .envrc)"
  if [[ "$existing_block" != "$desired_block" ]]; then
    sed -i.bak "/$start_marker/,/$end_marker/d" .envrc
    {
      printf "%s\n" "$start_marker"
      printf "%s\n" "$content"
      printf "%s\n" "$end_marker"
    } >> .envrc
  fi
}

direnv-provision-python "$@"

Git default exclusion patterns

**/.tool-versions
*.SAFE
*.class
*.egg-info
*.orig
*.porig
*.pyc
*.rej
*.swp
*.un~
*_BACKUP_*
*_BASE_*
*_LOCAL_*
*_REMOTE_*
.akku
.basedpyright
.clj-kondo
.direnv
.direnv/python-*
.envrc
.envrc.bak
.git
.git_worktree_hooks
.gitconfig
.idea
.lsp
.npm
.opencode
.ropeproject
.tags
.venv
.vscode
/tmp
AGENTS.md
PROJECT.md
Session.vim
__pycache__
bun.lock*
generated
node_modules
nohup.out
openspec
package-lock.json
package.json
profile.log
scheme-docs/resources
session-ses_*.md
site-packages
tags
tags.tmp
target
uv.lock
venv
vilog
xaa
~/.secrets
~/.vim
~/.vim/plugged
~/.win/
~/Games
~/tmp/.vim

Published on 2026-02-21T10:36:26.199215Z
Last updated on 2026-02-21T10:36:26.199586Z