Language-agnostic project detection plugin for Zsh with customizable on_project and
off_project hooks.
- π Smart Detection: Automatically detects project directories by common markers
(
.git,pyproject.toml,package.json, etc.) - β‘ Performance: Efficient implementation with minimal overhead (~1-5ms per directory change)
- π― Language Agnostic: Works with any project type - Python, Node.js, Rust, Go, Java, PHP, Ruby, etc.
- πͺ Customizable Hooks: Define custom behavior when entering/leaving projects
- π Project Root Detection: Walks up directory tree to find project boundaries
- π« Path Filtering: Blacklist/whitelist support to avoid unwanted triggers
- π¨ PowerLevel10k Integration: Built-in
hookie_dirsegment with intelligent path shortening - π Smart Directory Display: Project-aware path shortening with color coding
- β‘ Smart
cdCommand: Emptycdgoes to project root instead of home - π Message Control: Configurable notifications for project enter/leave events
- π Comprehensive Coverage: Supports 100+ project markers across all major languages and tools
Oh My Zsh:
git clone https://github.com/yourusername/zsh-hookie-projects ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-hookie-projects
# Add 'zsh-hookie-projects' to plugins array in ~/.zshrcZinit:
zinit load "yourusername/zsh-hookie-projects"Manual:
git clone https://github.com/yourusername/zsh-hookie-projects ~/.zsh/zsh-hookie-projects
echo "source ~/.zsh/zsh-hookie-projects/zsh-hookie-projects.plugin.zsh" >> ~/.zshrcThe plugin works automatically once installed. Navigate between directories and watch the hooks trigger:
β― cd ~/projects/zsh-hookie-projects
π Entered project: zsh-hookie-projects (.git, README.md)
β― cd functions
~/p/zsh-hookie-projects/functions main β‘ 1 !3 βͺ2
β― cd
π Going to project root: zsh-hookie-projects
~/p/zsh-hookie-projects main β‘ 1 !3 βͺ2
β― cd ~
π Left project: zsh-hookie-projects
~The plugin automatically sets these environment variables when entering projects:
HOOKIE_CURRENT_PROJECT- Project name (e.g.,my-project)HOOKIE_PROJECT_ROOT- Full path to project rootHOOKIE_PROJECT_MARKERS_STRING- Comma-separated detected markers
β― echo $HOOKIE_CURRENT_PROJECT
zsh-hookie-projects
β― echo $HOOKIE_PROJECT_ROOT
/home/user/projects/zsh-hookie-projects
β― echo $HOOKIE_PROJECT_MARKERS_STRING
.git, README.md, zsh-hookie-projects.plugin.zshReplace the default dir segment with hookie_dir for project-aware path display:
# In your ~/.p10k.zsh, replace 'dir' with 'hookie_dir'
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
# ... other elements ...
hookie_dir # Use instead of 'dir'
vcs
# ... other elements ...
)Features:
- Intelligent Path Shortening:
~/projects/my-appβ~/p/my-app - Color-Coded Paths: Project root in blue, subdirectories in cyan
- Copy-Pasteable: Paths work when copied to clipboard
- Fallback Support: Standard directory display when not in projects
Add a simple project indicator segment:
# Add to your prompt elements
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS+=(hookie_project)
# Simple project segment
function prompt_hookie_project() {
[[ -n "$HOOKIE_CURRENT_PROJECT" ]] && p10k segment -f 6 -t "$HOOKIE_CURRENT_PROJECT"
}function prompt_hookie_project() {
if [[ -n "$HOOKIE_CURRENT_PROJECT" ]]; then
local icon="β"
case "$HOOKIE_PROJECT_MARKERS_STRING" in
*"pyproject.toml"*) icon="π" ;; # Python
*"package.json"*) icon="π¦" ;; # Node.js
*"Cargo.toml"*) icon="π¦" ;; # Rust
*"go.mod"*) icon="πΉ" ;; # Go
*".git"*) icon="π" ;; # Git
esac
p10k segment -f 6 -i "$icon" -t "$HOOKIE_CURRENT_PROJECT"
fi
}Add to the bottom of your ~/.p10k.zsh:
##########################[ hookie_dir: current project + directory ]##########################
# Hookie-dir segment colors (customize as needed)
typeset -g HOOKIE_DIR_PROJECT_COLOR=4 # Blue for project paths
typeset -g HOOKIE_DIR_SUBDIR_COLOR=6 # Cyan for subdirectories
typeset -g HOOKIE_DIR_DEFAULT_COLOR=4 # Blue for non-project directoriesThe plugin overrides the cd command to provide project-aware navigation:
β― cd ~/projects/my-app
π Entered project: my-app
β― cd src/components
~/p/my-app/src/components
β― cd # Goes to project root instead of HOME!
π Going to project root: my-app
~/p/my-app
β― cd ~ # Explicit ~ still works
π Left project: my-app
~# Disable smart cd behavior
export HOOKIE_DISABLE_SMART_CD=1Configure which messages are displayed:
# Message control flags (set to 0 to disable, 1 to enable)
export HOOKIE_SHOW_ENTERING=1 # Show "π Entered project"
export HOOKIE_SHOW_LEAVING=1 # Show "π Left project"
export HOOKIE_SHOW_CD_PROJECT_ROOT=1 # Show "π Going to project root"# Completely silent operation
export HOOKIE_SHOW_ENTERING=0
export HOOKIE_SHOW_LEAVING=0
export HOOKIE_SHOW_CD_PROJECT_ROOT=0# Add these convenience functions to ~/.zshrc
hookie_messages_on() {
export HOOKIE_SHOW_ENTERING=1
export HOOKIE_SHOW_LEAVING=1
export HOOKIE_SHOW_CD_PROJECT_ROOT=1
echo "π Hookie messages enabled"
}
hookie_messages_off() {
export HOOKIE_SHOW_ENTERING=0
export HOOKIE_SHOW_LEAVING=0
export HOOKIE_SHOW_CD_PROJECT_ROOT=0
echo "π Hookie messages disabled"
}Add your own project markers in ~/.zshrc:
# Add custom markers
HOOKIE_PROJECT_MARKERS+=(
'deno.json' # Deno
'requirements.txt' # Python legacy
'yarn.lock' # Yarn
'flake.nix' # Nix
'.project-root' # Custom marker
)Blacklist Mode (default) - Block specific directories:
# Add to ~/.zshrc to customize blacklist
HOOKIE_BLACKLIST_PATHS+=(
"$HOME/Downloads" # Downloads folder
"$HOME/Desktop" # Desktop
"$HOME/.Trash" # Trash
"/mnt" # Mount points
"$HOME/node_modules" # npm modules
)Whitelist Mode - Only allow specific directories:
# Enable whitelist mode by adding allowed paths
HOOKIE_WHITELIST_PATHS=(
"$HOME/projects" # Only allow ~/projects
"$HOME/work" # And ~/work
"$HOME/dev" # And ~/dev
"/workspace" # And /workspace
)Override the default hook functions to add your own behavior:
# Custom on_project hook
on_project_hook() {
local project_root="$1"
shift
local markers=("$@")
echo "π― Working on: ${project_root:t}"
# Auto-activate Python venv
if (( ${markers[(Ie)pyproject.toml]} )); then
[[ -d "$project_root/.venv" ]] && source "$project_root/.venv/bin/activate"
fi
# Load project environment
[[ -f "$project_root/.env" ]] && source "$project_root/.env"
# Set custom environment variables
export PROJECT_NAME="${project_root:t}"
export PROJECT_ROOT="$project_root"
}
# Custom off_project hook
off_project_hook() {
local project_root="$1"
echo "β¨ Goodbye ${project_root:t}"
# Cleanup
[[ -n "$VIRTUAL_ENV" ]] && deactivate 2>/dev/null
unset PROJECT_NAME PROJECT_ROOT
}Automatically activate Python virtual environments when entering Python projects:
HOOKIE_PROJECT() {
# Silent mode - no messages
export HOOKIE_SHOW_ENTERING=1
export HOOKIE_SHOW_LEAVING=0
export HOOKIE_SHOW_CD_PROJECT_ROOT=0
zinit load ~/projects/zsh-hookie-projects
on_project_hook() {
local project_root="$1"
shift
local markers=("$@")
# Set standard environment variables (silent)
export HOOKIE_CURRENT_PROJECT="${project_root:t}"
export HOOKIE_PROJECT_ROOT="$project_root"
typeset -g HOOKIE_PROJECT_MARKERS_STRING="${(j:, :)markers}"
export HOOKIE_PROJECT_MARKERS_STRING
# Check if it's a Python project
local python_markers=("pyproject.toml" "requirements.txt" "setup.py" ".venv" "venv" "env" "Pipfile")
local is_python=0
for marker in "${python_markers[@]}"; do
if (( ${markers[(Ie)$marker]} )); then
is_python=1
break
fi
done
if (( is_python )) && [[ -f "$project_root/.venv/bin/activate" ]]; then
# Python project with .venv
source .venv/bin/activate
v $(pwd)
else
# Any other project
v $(pwd)
fi
}
off_project_hook() {
local project_root="$1"
# Silent cleanup
unset HOOKIE_CURRENT_PROJECT
unset HOOKIE_PROJECT_ROOT
unset HOOKIE_PROJECT_MARKERS_STRING
HOOKIE_CURRENT_PROJECT_DIR=""
HOOKIE_CURRENT_PROJECT_MARKERS=()
}
}
HOOKIE_PROJECTFor a more minimal approach, just focusing on Python projects:
# Pre-hook: Always deactivate before entering any project
on_project__pre_hook() {
local project_root="$1"
# Deactivate any existing virtual environment before entering new project
deactivate 2>/dev/null || true
}
on_project_hook() {
local project_root="$1"
shift
local markers=("$@")
# Standard setup
export HOOKIE_CURRENT_PROJECT="${project_root:t}"
export HOOKIE_PROJECT_ROOT="$project_root"
typeset -g HOOKIE_PROJECT_MARKERS_STRING="${(j:, :)markers}"
export HOOKIE_PROJECT_MARKERS_STRING
echo "π Entered project: ${project_root:t}"
# Simple Python venv activation
if [[ -f "$project_root/.venv/bin/activate" ]]; then
echo "π Activating .venv"
source "$project_root/.venv/bin/activate"
elif [[ -f "$project_root/venv/bin/activate" ]]; then
echo "π Activating venv"
source "$project_root/venv/bin/activate"
fi
}
# Pre-hook: Always deactivate before leaving any project
off_project__pre_hook() {
deactivate 2>/dev/null || true
}
off_project_hook() {
local project_root="$1"
echo "π Left project: ${project_root:t}"
# Deactivate any active virtual environment
[[ -n "$VIRTUAL_ENV" ]] && deactivate 2>/dev/null
# Cleanup
unset HOOKIE_CURRENT_PROJECT HOOKIE_PROJECT_ROOT HOOKIE_PROJECT_MARKERS_STRING
}The plugin detects projects by looking for these files and directories:
.git,.gitignore,.gitmodules
pyproject.toml,requirements.txt,setup.py,setup.cfg,Pipfile,poetry.lock,.venv,venv,env,.python-version,tox.ini,pytest.ini
package.json,package-lock.json,yarn.lock,pnpm-lock.yaml,.nvmrc,tsconfig.json,webpack.config.js,vite.config.js,.eslintrc.js,.prettierrc,jest.config.js
- Rust:
Cargo.toml,Cargo.lock - Go:
go.mod,go.sum,go.work - Java:
pom.xml,build.gradle,build.gradle.kts - PHP:
composer.json,composer.lock - Ruby:
Gemfile,Gemfile.lock,Rakefile - Elixir:
mix.exs,mix.lock - Gleam:
gleam.toml - Deno:
deno.json,deno.jsonc - Swift:
Package.swift - Dart/Flutter:
pubspec.yaml
- Docker:
Dockerfile,docker-compose.yml,.dockerignore - Infrastructure:
terraform,Vagrantfile,ansible - CI/CD:
.github,.gitlab-ci.yml,Jenkinsfile,.circleci
- Config:
.env,config.toml,config.yaml,.editorconfig - Docs:
README.md,README.rst,LICENSE,CHANGELOG.md,docs - IDE:
.vscode,.idea,.vim,.nvim
- Build:
Makefile,CMakeLists.txt,gulpfile.js,webpack.config.js - Database:
schema.sql,migrations,alembic.ini - Package Managers:
Brewfile,Podfile,flake.nix
And many more! See the full list in the plugin source.
These paths are blacklisted by default (exact matches only) to prevent false positives:
- System directories:
/,/usr,/opt,/var,/etc,/tmp - macOS system:
/System,/Library - User config:
~/.config,~/.cache,~/.local - Development tools:
~/.oh-my-zsh,~/.zinit,~/.npm,~/.cargo - User folders:
~/Desktop,~/Downloads,~/Documents
- Efficient Detection: ~1-5ms overhead per directory change
- Smart Caching: Only triggers hooks when project context changes
- Minimal File I/O: Optimized file existence checks
- No Background Processes: All operations are synchronous and fast
- Path Shortening: Intelligent parent directory compression
zsh-hookie-projects/
βββ zsh-hookie-projects.plugin.zsh # Main plugin file
βββ functions/ # Function directory
β βββ _hookie_find_project_root # Project detection logic
β βββ _hookie_detect_markers # Marker detection
β βββ _hookie_check_project_change # State management
β βββ _hookie_is_path_allowed # Path filtering
β βββ on_project_hook # Enter project hook
β βββ off_project_hook # Leave project hook
β βββ prompt_hookie_dir # PowerLevel10k directory segment
β βββ cd # Smart cd command
βββ README.md # Documentation
βββ LICENSE # MIT License# Path filtering
HOOKIE_BLACKLIST_PATHS=(...) # Blocked directories
HOOKIE_WHITELIST_PATHS=(...) # Allowed directories (optional)
# Project detection
HOOKIE_PROJECT_MARKERS=(...) # File/directory markers
# Directory display colors
HOOKIE_DIR_PROJECT_COLOR=4 # Project path color
HOOKIE_DIR_SUBDIR_COLOR=6 # Subdirectory color
HOOKIE_DIR_DEFAULT_COLOR=4 # Non-project directory color
# Message control
HOOKIE_SHOW_ENTERING=1 # Show enter messages
HOOKIE_SHOW_LEAVING=1 # Show leave messages
HOOKIE_SHOW_CD_PROJECT_ROOT=1 # Show smart cd messages
# Behavior control
HOOKIE_DISABLE_SMART_CD=0 # Disable smart cd commandMake sure the plugin is properly loaded and functions are available:
# Check if functions are loaded
type prompt_hookie_dir
type _hookie_check_project_change
# Manually reload if needed
autoload -Uz prompt_hookie_dirIf the directory segment shows empty on first terminal startup, simply press Enter once. This is a minor initialization timing issue with PowerLevel10k.
If you see inconsistent type for assignment errors in custom hooks:
# Use this pattern in custom hooks:
typeset -g HOOKIE_PROJECT_MARKERS_STRING="${(j:, :)markers}"
export HOOKIE_PROJECT_MARKERS_STRINGEnable debug output to troubleshoot issues:
# Add to your hook functions temporarily
echo "DEBUG: Project root: $project_root"
echo "DEBUG: Markers: ${markers[@]}"
echo "DEBUG: PWD: $PWD"Contributions are welcome! Please feel free to:
- Add support for new project markers
- Improve performance optimizations
- Fix bugs or edge cases
- Enhance documentation
- Add new PowerLevel10k segment features
MIT License - see LICENSE file.
Inspired by various zsh plugins and the need for a language-agnostic project detection system. Built for developers who work with multiple programming languages and want consistent project-aware shell behavior.
Special thanks to the PowerLevel10k project for providing the excellent prompt framework
that makes the hookie_dir segment possible.