๐ช zsh-hookie-projects
March 17, 2026 ยท View on GitHub
Language-agnostic project detection plugin for Zsh with customizable on_project and
off_project hooks.
Features
- ๐ 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
Installation
Using a Plugin Manager
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 ~/.zshrc
Zinit:
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" >> ~/.zshrc
Usage
The 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
~
Environment Variables
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.zsh
PowerLevel10k Integration
Enhanced Directory Segment
Replace 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
Project Name Segment
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"
}
With Dynamic Icons
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
}
Configure Colors
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 directories
Smart cd Command
The 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
# Disable smart cd behavior
export HOOKIE_DISABLE_SMART_CD=1
Message Control
Configure 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"
Silent Mode
# Completely silent operation
export HOOKIE_SHOW_ENTERING=0
export HOOKIE_SHOW_LEAVING=0
export HOOKIE_SHOW_CD_PROJECT_ROOT=0
Quick Toggle Functions
# 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"
}
Customization
Custom Project Markers
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
)
Path Filtering
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
)
Custom Hooks
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
}
Auto-Activate Python Virtual Environment
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_PROJECT
Simple Python Virtual Environment Example
For 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
}
Default Project Markers
The plugin detects projects by looking for these files and directories:
Version Control
.git,.gitignore,.gitmodules
Python
pyproject.toml,requirements.txt,setup.py,setup.cfg,Pipfile,poetry.lock,.venv,venv,env,.python-version,tox.ini,pytest.ini
JavaScript/Node.js
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
Other Languages
- 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
Infrastructure & DevOps
- Docker:
Dockerfile,docker-compose.yml,.dockerignore - Infrastructure:
terraform,Vagrantfile,ansible - CI/CD:
.github,.gitlab-ci.yml,Jenkinsfile,.circleci
Configuration & Documentation
- Config:
.env,config.toml,config.yaml,.editorconfig - Docs:
README.md,README.rst,LICENSE,CHANGELOG.md,docs - IDE:
.vscode,.idea,.vim,.nvim
Build Tools & Databases
- 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.
Default Blacklisted Paths
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
Performance
- 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
Advanced Features
Plugin Structure
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
Configuration Variables
# 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 command
Troubleshooting
Plugin Not Loading
Make 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_dir
Startup Delay
If the directory segment shows empty on first terminal startup, simply press Enter once. This is a minor initialization timing issue with PowerLevel10k.
Export Type Errors
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_STRING
Debug Mode
Enable 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"
Contributing
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
License
MIT License - see LICENSE file.
Credits
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.