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

HookWhen it runsUse cases
pre_gen_project.pyBefore creating the project directoryValidate inputs, check dependencies
post_gen_project.pyAfter creating the project, before returning itDelete 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

  1. Fail fast: Validate everything in pre_gen_project.py before generating.
  2. Clear messages: Use print() with ERROR: and sys.exit(1) for failures.
  3. Idempotency: Hooks should be able to run multiple times without error.
  4. Don't overwrite: Use shutil.which() before running external commands.
  5. Cross-platform: Use sys.platform for OS-specific paths and commands.

Next Steps