Hooks Guide
July 6, 2026 · View on GitHub
Hooks are Python scripts that run at specific points during the generation process. They allow you to validate inputs, modify files, initialize git, install dependencies, and more.
Hook Types
| Hook | When it runs | Use cases |
|---|---|---|
pre_gen_project.py | Before creating the project directory | Validate inputs, check dependencies |
post_gen_project.py | After creating the project, before returning it | Delete conditional files, git init, submodules |
Execution Order
1. User runs cookiecutter
2. Variables are collected (interactive or --no-input)
3. ▶ pre_gen_project.py ← pre hook
4. Project directory is created
5. Files are rendered with Jinja2
6. ▶ post_gen_project.py ← post hook
7. Project is ready
Structure
my-template/
├── cookiecutter.json
├── hooks/
│ ├── pre_gen_project.py
│ └── post_gen_project.py
└── {{ cookiecutter.project_name }}/
└── ...
pre_gen_project.py
Runs before the project is created. If it raises an exception, generation is canceled.
Example: validate project name
import re
import sys
MODULE_REGEX = r'^[_a-zA-Z][_a-zA-Z0-9]+$'
MODULE_NAME = '{{ cookiecutter.project_slug }}'
if not re.match(MODULE_REGEX, MODULE_NAME):
print(f'ERROR: "{MODULE_NAME}" is not a valid name. '
f'Must start with a letter or _ and contain only alphanumeric characters and _.')
sys.exit(1)
Example: check Docker is installed
import shutil
import sys
if '{{ cookiecutter.use_docker }}' == 'yes':
if not shutil.which('docker'):
print('ERROR: Docker is not installed but you selected use_docker=yes.')
sys.exit(1)
Example: validate directory does not exist
import os
import sys
project_dir = '{{ cookiecutter.project_name }}'
if os.path.exists(project_dir):
print(f'ERROR: Directory "{project_dir}" already exists.')
sys.exit(1)
post_gen_project.py
Runs after the project is created. The working directory (cwd) is the newly created project. If it raises an exception, the project is deleted (unless --keep-project-on-failure is used).
Example: delete conditional files
import os
import shutil
use_docker = '{{ cookiecutter.use_docker }}' == 'yes'
use_ci = '{{ cookiecutter.use_ci }}' == 'yes'
if not use_docker:
os.remove('Dockerfile')
shutil.rmtree('docker/')
if not use_ci:
shutil.rmtree('.github/workflows/')
Example: initialize git
import subprocess
subprocess.run(['git', 'init'], check=True)
subprocess.run(['git', 'add', '.'], check=True)
subprocess.run(['git', 'commit', '-m', 'Initial commit'], check=True)
Example: create virtual environment and install dependencies
import subprocess
import sys
subprocess.run([sys.executable, '-m', 'venv', 'venv'], check=True)
pip = 'venv/Scripts/pip' if sys.platform == 'win32' else 'venv/bin/pip'
subprocess.run([pip, 'install', '-r', 'requirements.txt'], check=True)
Example: download external dependencies
import subprocess
import urllib.request
# Download a file
url = 'https://raw.githubusercontent.com/user/repo/main/config.yaml'
urllib.request.urlretrieve(url, 'config/downloaded_config.yaml')
# Clone a submodule
subprocess.run(['git', 'clone', 'https://github.com/user/submodule.git', 'lib/submodule'], check=True)
Accessing Variables in Hooks
Inside hooks, cookiecutter.json variables are available as rendered strings:
# hooks/post_gen_project.py
project_name = '{{ cookiecutter.project_name }}'
project_slug = '{{ cookiecutter.project_slug }}'
license = '{{ cookiecutter.license }}'
print(f'Project {project_name} generated successfully.')
Important: Values are rendered strings, not Python objects. To compare booleans, use strings:
'{{ cookiecutter.use_docker }}' == 'yes'.
Hooks with Complex Logic
For complex logic, it's better to import modules from the hook itself:
# hooks/post_gen_project.py
import os
import sys
# Add the hooks directory to the path for imports
sys.path.insert(0, os.path.dirname(__file__))
from setup_utils import configure_database, setup_ci # noqa: E402
configure_database('{{ cookiecutter.database }}')
setup_ci('{{ cookiecutter.ci_provider }}')
# hooks/setup_utils.py
import os
import shutil
def configure_database(db_type):
if db_type == 'sqlite':
os.remove('config/postgresql.yaml')
elif db_type == 'postgresql':
os.remove('config/sqlite.yaml')
def setup_ci(provider):
if provider == 'github':
shutil.rmtree('.gitlab/')
elif provider == 'gitlab':
shutil.rmtree('.github/')
Debugging Hooks
# hooks/post_gen_project.py
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.debug('Variables: project_name={{ cookiecutter.project_name }}')
logger.debug('CWD: %s', os.getcwd())
Run with verbose:
cookiecutter --verbose ./my-template
Best Practices
- Fail fast: Validate everything in
pre_gen_project.pybefore generating. - Clear messages: Use
print()withERROR:andsys.exit(1)for failures. - Idempotency: Hooks should be able to run multiple times without error.
- Don't overwrite: Use
shutil.which()before running external commands. - Cross-platform: Use
sys.platformfor OS-specific paths and commands.