Skip to content

Fix: Cookiecutter Not Working — Template Errors, Variable Substitution, and Hook Failures

FixDevs ·

Quick Answer

How to fix Cookiecutter errors — cookiecutter.json not found, variable substitution failed Jinja2, pre/post-generation hooks failed, no_input mode missing values, private repo authentication, and copier vs cookiecutter.

The Error

You try to generate a project from a template and it fails:

$ cookiecutter https://github.com/me/my-template
A valid repository for "https://github.com/me/my-template" could not be found

Or template variables don’t substitute:

# {{ cookiecutter.project_name }}/main.py
app_name = "{{ cookiecutter.project_name }}"   # Renders literally instead of substituting

Or pre-generation hooks fail with no clear error:

$ cookiecutter my-template
ERROR: Stopping generation because pre_gen_project hook script didn't exit successfully
# No further details

Or the generated directory has a literal {{ cookiecutter.project_slug }} folder name:

my-template/
└── {{ cookiecutter.project_slug }}/   ← Literal name, not substituted
    └── main.py

Or --no-input mode skips required variables and breaks:

$ cookiecutter --no-input my-template
# Uses defaults — but a required variable had no default, breaks silently

Cookiecutter is the standard Python project scaffolder — point it at a template (local dir or git URL), answer prompts, and get a generated project. It’s used by Django, Flask, FastAPI, and most Python framework starter kits. The Jinja2-based templating is powerful but template authors hit specific issues around variable scoping, hook execution, and the difference between development and use. This guide covers each.

Why This Happens

Cookiecutter renders a template directory using Jinja2 syntax. The template root must contain cookiecutter.json (defining variables) and at least one directory named with a Jinja2 expression (typically {{ cookiecutter.project_slug }}). Cookiecutter prompts the user for variable values, then renders every file and directory name through Jinja2.

The dual nature — template directory containing literal Jinja2 syntax that becomes a real project after rendering — confuses both template users and template authors.

Fix 1: Installing and Basic Use

pip install cookiecutter
# Or via pipx (recommended for global use)
pipx install cookiecutter

Generate from a template:

# From a git URL
cookiecutter https://github.com/cookiecutter/cookiecutter-django

# From a local directory
cookiecutter ./my-template/

# From a specific branch or tag
cookiecutter https://github.com/me/my-template --checkout v2.0

# Skip prompts (use defaults from cookiecutter.json)
cookiecutter ./my-template/ --no-input

# Override specific values
cookiecutter ./my-template/ project_name="My Project" author_name="Alice"

Common Mistake: Pointing cookiecutter at a non-template repo. The repo must contain cookiecutter.json at the root — without it, cookiecutter fails with “valid repository could not be found.” Always verify the repo has a cookiecutter.json before trying to use it as a template.

Generate without prompts in CI:

cookiecutter my-template/ --no-input \
    project_name="My App" \
    project_slug=my_app \
    author_name="CI Bot"

Fix 2: cookiecutter.json — Variables and Defaults

{
  "project_name": "My Awesome Project",
  "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}",
  "author_name": "Your Name",
  "email": "[email protected]",
  "version": "0.1.0",
  "license": ["MIT", "BSD-3-Clause", "Apache-2.0"],
  "use_docker": ["yes", "no"],
  "_copy_without_render": [
    "*.html",
    "frontend/templates/*"
  ]
}

Variable types:

TypeExampleMeaning
String"My Project"Free-text input, default value
Choice list["MIT", "BSD"]First value is default; user picks from list
Derived"{{ cookiecutter.x }}_suffix"Computed from other vars
Boolean (yes/no)["yes", "no"]Choice list with yes/no

Derived variables use Jinja2 — useful for computed defaults:

{
  "project_name": "My Project",
  "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}",
  "package_name": "{{ cookiecutter.project_slug }}",
  "year": "2025"
}

When the user types “Hello World” for project_name, project_slug auto-computes to hello_world.

Private variables (start with _) don’t prompt the user:

{
  "project_name": "...",
  "_copy_without_render": ["docs/*.html"],
  "_extensions": ["jinja2_time.TimeExtension"]
}

_copy_without_render lists patterns NOT rendered through Jinja2 — useful for files that contain {{ syntax for other tools (HTML templates, Jinja files in the generated project).

Common Mistake: Forgetting _copy_without_render for HTML or Jinja templates in the generated project. Cookiecutter tries to render them and fails on syntax conflicts. List them in _copy_without_render to copy verbatim.

Fix 3: Template Directory Structure

my-template/
├── cookiecutter.json
├── hooks/
│   ├── pre_gen_project.py     # Optional: runs BEFORE generation
│   └── post_gen_project.py    # Optional: runs AFTER generation
└── {{ cookiecutter.project_slug }}/   # The actual template directory
    ├── README.md
    ├── pyproject.toml
    ├── {{ cookiecutter.package_name }}/
    │   ├── __init__.py
    │   └── main.py
    └── tests/
        └── test_main.py

The top-level template directory MUST use Jinja2 syntax for its name:

my-template/
├── cookiecutter.json
└── {{ cookiecutter.project_slug }}/   ← Must be a Jinja2 expression
    └── ...

Without this, cookiecutter generates a literal {{ cookiecutter.project_slug }} directory.

Pro Tip: Always name your top-level template dir {{ cookiecutter.project_slug }} (not {{ cookiecutter.project_name }}). Names contain spaces and special chars; slugs are filesystem-safe. Using project_name produces directories like My Project/ with a space — works on Linux/macOS but trips up many build tools.

File names also support Jinja2:

{{ cookiecutter.project_slug }}/
├── {{ cookiecutter.package_name }}.py    # File name templated
└── {% if cookiecutter.use_docker == 'yes' %}Dockerfile{% endif %}

Conditional files (file generated only if condition is true) use {% if %} in the name. If the expression evaluates to empty, no file is created.

Fix 4: Jinja2 Templating in Files

Inside files, use standard Jinja2:

# {{ cookiecutter.project_slug }}/main.py
"""{{ cookiecutter.project_name }}

Authored by {{ cookiecutter.author_name }} <{{ cookiecutter.email }}>
"""

VERSION = "{{ cookiecutter.version }}"

{% if cookiecutter.use_docker == "yes" %}
import os
DOCKER_MODE = True
{% else %}
DOCKER_MODE = False
{% endif %}

def main():
    print(f"Hello from {VERSION}")

if __name__ == "__main__":
    main()

Common Jinja2 patterns:

{# This is a Jinja2 comment, doesn't appear in output #}

{# Conditionals #}
{% if cookiecutter.framework == "fastapi" %}
import fastapi
{% endif %}

{# Loops #}
{% for dep in cookiecutter.dependencies.split(",") %}
{{ dep.strip() }}
{% endfor %}

{# String manipulation #}
{{ cookiecutter.project_name | upper }}
{{ cookiecutter.project_name | replace(" ", "-") }}
{{ cookiecutter.project_name | length }}

Common Mistake: Forgetting that Cookiecutter renders every text file as Jinja2. If your template contains a file with literal {{ or {% (e.g., a Vue.js template, Django template, GitHub Actions workflow with ${{ }}), the render fails or produces wrong output. Either:

  1. Add the path to _copy_without_render in cookiecutter.json
  2. Escape: {{ "{{" }} produces literal {{
  3. Use {% raw %}...{% endraw %} blocks for sections that should pass through
{# Escape for GitHub Actions syntax #}
{% raw %}
on:
  push:
    branches: [main]
env:
  VERSION: ${{ github.sha }}
{% endraw %}

Fix 5: Pre and Post-Generation Hooks

Pre-generation hook runs before rendering — validate inputs, transform variables:

# hooks/pre_gen_project.py
import re
import sys

PROJECT_SLUG = "{{ cookiecutter.project_slug }}"

if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", PROJECT_SLUG):
    print(f"ERROR: '{PROJECT_SLUG}' is not a valid Python module name")
    sys.exit(1)

Exit non-zero to abort generation.

Post-generation hook runs after rendering — initialize git, install deps, remove unwanted files:

# hooks/post_gen_project.py
import os
import shutil
import subprocess

USE_DOCKER = "{{ cookiecutter.use_docker }}" == "yes"

if not USE_DOCKER:
    if os.path.exists("Dockerfile"):
        os.remove("Dockerfile")
    if os.path.exists(".dockerignore"):
        os.remove(".dockerignore")

# Initialize git
subprocess.run(["git", "init"], check=True)
subprocess.run(["git", "add", "."], check=True)
subprocess.run(["git", "commit", "-m", "Initial commit"], check=True)

print("Project created! Next steps:")
print("  cd {{ cookiecutter.project_slug }}")
print("  pip install -e .")

Hooks run in the generated directory for post-gen and the template directory for pre-gen. Their working directory matters.

Common Mistake: Using cookiecutter syntax {{ }} in hook scripts and being confused when it works — yes, hook scripts are also rendered through Jinja2 before execution. You can use {{ cookiecutter.use_docker }} directly in hooks/post_gen_project.py — cookiecutter substitutes the value before running the script. Don’t escape it; it’s intended.

Fix 6: Choice Variables and Conditional File Removal

{
  "project_name": "My App",
  "framework": ["fastapi", "flask", "django"],
  "include_docs": ["yes", "no"]
}

Use the choice in templates:

# Inside a file
{% if cookiecutter.framework == "fastapi" %}
import fastapi
{% elif cookiecutter.framework == "flask" %}
import flask
{% endif %}

Conditional directories — directory name evaluates to empty string when not needed:

{{ cookiecutter.project_slug }}/
├── {% if cookiecutter.include_docs == 'yes' %}docs{% endif %}/
└── src/

When include_docs == "no", the docs/ directory is named "" (empty) — cookiecutter skips creating it.

Cleaner pattern with post-gen hook:

# hooks/post_gen_project.py
import shutil

if "{{ cookiecutter.include_docs }}" == "no":
    shutil.rmtree("docs")

if "{{ cookiecutter.framework }}" != "django":
    shutil.rmtree("django_settings", ignore_errors=True)

Easier to maintain than complex Jinja2 expressions in file names.

Fix 7: Template Inheritance and Extensions

For complex templates, use extensions:

{
  "_extensions": [
    "jinja2_time.TimeExtension",
    "slugify.slugify"
  ]
}
pip install jinja2-time

Then in templates:

{# Current year #}
{% now 'utc', '%Y' %}

{# Slugify a string #}
{{ cookiecutter.project_name | slugify }}

Reusable templates — split into a base + variants. Two main approaches:

  1. Multiple cookiecutter templates sharing common files via symlinks or git submodules
  2. Copier (alternative tool) — supports template inheritance natively

For more advanced templating with versioning and updates after generation, see Copier — it supports updating already-generated projects when the template changes, which cookiecutter doesn’t.

Fix 8: Private Repos and Authentication

# SSH
cookiecutter [email protected]:me/private-template.git

# HTTPS with token
cookiecutter https://oauth2:[email protected]/me/private-template.git

# Cached templates
cookiecutter cookiecutter-pypackage   # Use abbreviation if in config

~/.cookiecutterrc for abbreviations and defaults:

default_context:
  full_name: "Your Name"
  email: "[email protected]"
  github_username: "yourname"

cookiecutters_dir: "~/.cookiecutters/"
replay_dir: "~/.cookiecutter_replay/"

abbreviations:
  pypackage: https://github.com/audreyfeldroy/cookiecutter-pypackage.git
  django: https://github.com/cookiecutter/cookiecutter-django.git
  gh: https://github.com/{0}.git

Then:

cookiecutter pypackage              # Resolves to the full URL
cookiecutter gh:me/my-template      # Custom abbreviation

default_context values pre-fill prompts — useful for personal info you set once.

Cache management:

# Templates cached at ~/.cookiecutters/
ls ~/.cookiecutters/

# Force re-download (skip cache)
cookiecutter my-template --no-cache

Still Not Working?

Cookiecutter vs Copier vs Yeoman

  • Cookiecutter — Python-native, Jinja2 templating, simple, mature. Best for one-shot project generation.
  • Copier — Python-native too, supports template updates after generation. Best for templates that evolve.
  • Yeoman — JavaScript-based, large ecosystem. Best for JS/frontend projects.

For Python projects, Cookiecutter is the dominant standard. Copier is gaining traction for templates that need to support “update existing project to latest template version.”

Testing Templates

pip install pytest-cookies
# tests/test_template.py
def test_default(cookies):
    result = cookies.bake(extra_context={"project_name": "Test Project"})
    assert result.exit_code == 0
    assert result.project_path.is_dir()
    assert (result.project_path / "README.md").exists()

def test_docker_option(cookies):
    result = cookies.bake(extra_context={"use_docker": "yes"})
    assert (result.project_path / "Dockerfile").exists()

For pytest fixture patterns with cookiecutter testing, see pytest fixture not found.

CI for Template Repos

# .github/workflows/test.yml
name: Test Template

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
      - run: pip install cookiecutter pytest pytest-cookies
      - run: pytest tests/
      # Then bake the template and run the generated project's tests
      - run: |
          cookiecutter . --no-input --output-dir generated/
          cd generated/*/
          pip install -e .
          pytest

This catches templates that bake successfully but produce broken projects.

Common Generated Project Setups

Common post-gen patterns include initializing git, creating a virtualenv, installing deps. For uv-based project setup, see uv not working. For Hatch-based packaging in generated projects, see Hatch not working.

Cookiecutter Templates Worth Knowing

  • cookiecutter-pypackage — Standard Python package
  • cookiecutter-django — Django web app
  • cookiecutter-data-science — Data science project structure
  • cookiecutter-flask — Flask web app
  • cookiecutter-pytorch — PyTorch ML project

For Django-specific setup that often starts from a cookiecutter template, see Django migration conflict.

Combining with pre-commit Hooks

Generated projects often include .pre-commit-config.yaml. The post-gen hook can install hooks automatically:

# hooks/post_gen_project.py
import subprocess

subprocess.run(["git", "init"], check=True)
subprocess.run(["pre-commit", "install"], check=False)
# check=False — don't fail if pre-commit isn't installed globally

For pre-commit setup patterns that work in generated projects, see pre-commit not working.

Updating Generated Projects

Cookiecutter doesn’t support updates — once generated, the project is independent. For long-lived projects that need to track template updates, use Copier (mentioned above) or maintain a cruft overlay:

pip install cruft

# Generate and track
cruft create https://github.com/me/my-template

# Later, pull template updates
cd generated-project/
cruft update

cruft is built on cookiecutter — same templates work, with the addition of update tracking via a .cruft.json file.

Replay Mode

cookiecutter saves all user inputs to ~/.cookiecutter_replay/ after each generation. Re-run with the same inputs:

cookiecutter my-template --replay

Useful for re-generating after fixing the template — same context, no need to re-enter every prompt.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles