Copier — template authoring and project lifecycle
Copier renders project templates (Jinja + YAML questionnaire) and manages the
lifecycle of generated projects. Two audiences: template authors (create/maintain
templates) and consumers (copy/update projects).
1. Core operations — use the right one
| Command |
Purpose |
When to use |
copier copy <src> <dst> |
Generate new project |
First render; also overlays onto preexisting dir |
copier update (run inside project) |
Smart update to newer template |
Template evolved; preserves local edits via 3-way merge |
copier recopy |
Dumb re-render, keep answers, discard history |
Broken update, deleted-file recovery, or update algorithm can't run |
copier check-update |
Report if template has newer version |
Manual (plain) or CI (--output-format json / --quiet exit 2 = update available) |
Common flags (copy):
copier copy --trust <src> <dst> # required if template has _tasks/_migrations/_jinja_extensions
copier copy --trust --defaults <src> <dst> # non-interactive, all defaults
copier copy -d 'key=value' -d 'list=[a, b]' <src> <dst> # override answers
copier copy --data-file answers.yml <src> <dst> # bulk answers (--data wins on conflict)
copier copy --vcs-ref HEAD <src> <dst> # dev: include dirty/unreleased changes
copier copy --vcs-ref v2.0.0 <src> <dst> # pin version
copier copy --skip-tasks <src> <dst> # skip _tasks (NOT migrations)
copier copy --pretend <src> <dst> # dry run
copier copy --overwrite <src> <dst> # overwrite without asking
copier copy -f <src> <dst> # = --defaults --overwrite
Update flags (run in project dir, clean git status first):
copier update --trust # standard
copier update --trust --defaults # reuse all prior answers
copier update --trust --defaults -d 'q=new' # change one answer only
copier update --vcs-ref=:current: # re-answer questions, keep template version
copier update --conflict rej|inline # conflict style (default inline)
copier update --skip-answered # keep recorded answers, don't re-ask
2. Template anatomy
my-template/ # usually a Git repo with PEP 440 tags (v1.0.0)
├── copier.yaml (or copier.yml) # questions + _settings (underscore-prefixed)
├── template/ # actual payload when _subdirectory: template
│ ├── {{ _copier_conf.answers_file }}.jinja
│ ├── README.md.jinja # *.jinja → rendered, suffix stripped
│ └── .gitignore # no suffix → copied verbatim
└── includes/ (optional) # macros/partials — must be _excluded
Key settings in copier.yaml:
_min_copier_version: "9.0.0" # abort if installed copier is older
_subdirectory: template # isolate payload from template meta files
_templates_suffix: .jinja # which files Jinja renders ("" = render everything)
_answers_file: .copier-answers.yml
_preserve: [.copier-answers.yml]
_exclude: ["~*", "*.py[co]", __pycache__, "*.rej"]
_tasks: ["git init", "mise install"]
_message_after_copy: |
Your project "{{ project_name }}" was created. Run `mise run check`.
_message_after_update: |
Your project "{{ project_name }}" was updated. Resolve conflicts, then check.
_exclude vs _skip_if_exists vs _tasks-only-once:
_exclude: never copy (gitignore syntax via pathspec; ! negates).
Templatable. Patterns match destination paths (after .jinja stripping),
so *.bar already covers foo.bar.jinja → foo.bar; do NOT add *.bar.jinja.
Use _copier_operation == 'update' guard for copy-once files.
_skip_if_exists: copy once; never overwrite if present; recreate on
update if missing (good for generated secrets).
_exclude with update-guard: never re-render on update even if missing.
3. Questions — best practices
Order matters: questions are asked top-to-bottom; a default/validator/when
can only reference earlier answers.
project_name:
type: str
help: Human-readable project name
default: my base project
project_slug:
type: str
help: URL/filesystem-safe slug
default: "{{ project_name|lower|replace(' ', '-')|replace('_', '-') }}"
validator: "{% if not (project_slug | regex_search('^[a-z][a-z0-9-]+$')) %}Use lowercase, digits, dashes; start with a letter.{% endif %}"
use_ci:
type: bool
help: Add CI workflow?
default: true
ci_provider:
type: str
choices:
GitHub CI: github # key shown to user, VALUE stored in template
GitLab CI: gitlab
default: github # default must be the VALUE, not the key
when: "{{ use_ci }}" # skip unless use_ci is true
deploy_key:
type: str
secret: true # hidden prompt, excluded from answers file
default: "{{ _external_data.secrets.deploy_key | default('changeme', true) }}"
placeholder: "paste deploy key" # visual hint only, not a value
Rules:
type: str|int|float|bool|json|yaml|path (yaml default). Keep choice
values to one type; prefer str and convert in template code.
- Always give
help and a sane default (omit default only to force input).
--defaults fails on default-less questions unless -d supplies them.
validator: Jinja that renders empty = valid, non-empty = error message.
when: false (boolean) or templated string. Skipped questions are not
stored, but their default is in render context. Use when: false for computed
values; render {{ UNSET }} as default to leave the var undefined.
choices: default must match value type. For multiselect bracket values quote
explicitly: default: '["[", "]"]', CLI: -d 'brackets=["[", "]"]'.
secret: true requires a real default of the question's type; the value
never lands in the answers file. default: null does NOT satisfy this —
verified on Copier 9: copy --defaults crashes with
InvalidTypeError: Invalid answer "None" ... of type "str". Use a static
fallback or _external_data (see §5).
- Conditional/dynamic choices: either
validator per choice (visible but
disabled with message) or templated choices: | block (hidden). When mixing
both, wrap validator in {% raw %}...{% endraw %}.
- Templating is allowed only inside string values, only with
already-answered variables. Interactive answers are never re-rendered.
- Computed, non-asked value:
default: "{{ earlier_var + 1 }}" + when: false.
To freeze it across updates (e.g. copyright_year), also dump it explicitly
in the answers template (see §5).
- Prefer well-known user defaults names so
settings.yml reuse works:
user_name, user_email, github_user, gitlab_user.
4. Jinja rendering rules
- Rendered: files ending in
_templates_suffix (default .jinja) — suffix is
stripped on output. Everything else copied verbatim. If both README.md and
README.md.jinja exist, the non-suffixed one is ignored.
- Directory names are templated but must NOT end with the suffix.
- File/dir names,
_exclude/_skip_if_exists patterns, _messages_*,
_tasks, _migrations, question default/help/choices/validator/when can
all contain Jinja.
- Conditional file:
{% if use_precommit %}.pre-commit-config.yaml{% endif %}.jinja
— suffix stays outside the {% if %} or the file is not recognized.
Use single quotes in path conditions (double quotes are illegal on Windows).
- Multi-pattern conditional exclude: one list item can render a whole
newline-separated gitignore block.
{% yield item from list %}{{ item }}{% endyield %} in a path loops to
generate many files/dirs; loop vars are in scope inside generated files.
- Reuse snippets via
{% include 'partial.jinja' %} or
{% from 'macros.jinja' import thing %} (paths relative to template root).
Put partials in includes/ and _exclude it, or use _subdirectory so they
are never copied. In path names use pathjoin('includes','x.jinja') (POSIX
separator required).
- Builtins: all Jinja2 +
jinja2-ansible-filters (to_nice_yaml,
to_nice_json, regex_search, ans_random|hash('sha512') for secrets, ...).
_envops default keeps trailing newlines. Set
_envops: {undefined: jinja2.StrictUndefined} to fail fast on typos.
- Useful context:
_copier_answers (safe, serializable, has _commit,
_src_path), _copier_conf (has .data, .dst_path, .src_path,
.sep, .os, .answers_file — WARNING .data may contain secrets),
_folder_name, _copier_python, _copier_phase (prompt/tasks/migrate/render),
_copier_operation (copy/update — tasks/exclude only), _external_data,
UNSET.
_external_data: {namespace: relative/path.yml} lazily parsed as YAML.
Use for multi-template composition (read parent answers) or loading ignored
secrets. Paths outside project root require --trust.
5. Answers file — the update contract
Template must ship {{ _copier_conf.answers_file }}.jinja (default name
.copier-answers.yml) with exactly:
# Changes here will be overwritten by Copier
{{ _copier_answers|to_nice_yaml -}}
- Commit it in generated projects. Without it there is no smart update.
- NEVER edit it by hand — it makes Copier believe a different answer set
produced the project and corrupts future diffs. Change answers via
copier update --defaults -d 'q=new', never via editor.
- Secrets (
secret: true) are excluded automatically — that is why they need
_external_data round-tripping if they must persist.
- Multi-template projects: each template gets its own file
(
-a .copier-answers.main.yml, -a .copier-answers.ci.yml, ...) and is
updated independently.
6. Tasks and migrations (unsafe — need --trust)
_tasks:
- "git init"
- "git rev-parse --verify HEAD >/dev/null 2>&1 || git commit --allow-empty -m 'Init commit'"
- ["mise", "install"] # array form: no shell, no escaping bugs
- command: ["{{ _copier_python }}", task.py]
when: "{{ _copier_operation == 'copy' }}"
- command: rm {{ name }}/README.md
when: "{{ _copier_conf.os in ['linux', 'macos'] }}"
_migrations:
- version: v2.0.0 # run only when old < v2.0.0 <= new (PEP 440)
command: rm -rf ./old-folder
when: "{{ _stage == 'before' }}"
_tasks run after every copy and update. _migrations run only on
update (optionally version-gated, before/after stage via _stage).
--skip-tasks skips tasks but not migrations.
- Each item runs in its own subprocess with
$STAGE, $VERSION_FROM,
$VERSION_TO, $VERSION_CURRENT (+ PEP 440-normalized variants) in env.
Answers file is reloaded after before migrations, so they can rewrite answers.
- Keep tasks idempotent, fast, and offline-safe where possible; prefer array
form; gate OS-specific commands on
_copier_conf.os.
- Any use of tasks/migrations/
_jinja_extensions makes copier abort with
exit 4 unless consumer passes --trust/--UNSAFE (or marks the source in
trust: in settings.yml). Verified: without --trust Copier aborts
before rendering anything — it does NOT render files and silently skip
tasks. To render without running tasks: --trust --skip-tasks.
7. Versioning, update safety, conflict recovery
- Tag template releases with stable PEP 440 versions (
v1.0.0). Default copy
and update resolve to the latest tag, not the branch tip. Never move a
released tag; use branches or explicit --vcs-ref for moving refs.
--vcs-ref HEAD = current checkout including dirty files (needed for
local template dev). Without it, dirty files are silently ignored because a
tag is checked out instead (FAQ gotcha). --vcs-ref=:current: = re-ask
without changing version.
- Before
update: clean git status. Add merge-conflict guard hooks:
check-merge-conflict --assume-in-merge for inline, forbid *.rej for
rej style.
- How update works: regen old-tag template → diff vs current project → apply
pre-migrations → render new-tag template → replay diff → post-migrations.
Template-deleted-but-project-deleted paths stay deleted;
skip_if_exists
paths are always restored. If the old template can't be regenerated (missing
external resource, incompatible Jinja extension, ancient Copier), fall back
to copier recopy and resolve with git diff (loses smart merge for that run).
- Abort a bad update:
git reset; git checkout .; git clean -d -i
(checkout <branch> / merge --abort do NOT work).
8. Caveats and known-issue checklist (verify before shipping)
_exclude in YAML replaces defaults (you lose copier.yaml, .git, …).
CLI -x extends. With _subdirectory set to a real dir, default
_exclude becomes []. _exclude matches destination paths.
copier copy ./src ./dst on a dirty template checks out latest tag —
dirty files missing. Use -r HEAD while developing.
- Shallow template clones cause huge git CPU use — use full clones.
- Never put credentials in the source URL (
https://user:pass@…) — they are
recorded in _src_path in the answers file. Use SSH keys / credential helpers.
secret questions need a default; choices defaults must be values with
matching type; multiselect bracket literals need inner quotes.
when: false values aren't stored — explicitly merge them into the answers
dump if they must be frozen ({{ dict(_copier_answers, foo=foo)|to_nice_yaml }}).
- Referencing a later question, or templating a key (not value), or unquoted
default: {{ 'x' }} are all invalid — keep YAML valid, template values only.
force = skip prompts + overwrite; defaults = use defaults but still fail
on default-less questions; overwrite = overwrite files only.
cleanup_on_error deletes dst only if Copier created it.
preserve_symlinks: false (default) replaces links with target content.
- Copier ≤5 used
.tmpl suffix and [[ ]] delimiters. For cross-version
templates pin _min_copier_version and, if needed, set _envops to the
legacy delimiters. Copier 7+ ignores the legacy fallback.
_jinja_extensions code runs at render — audit it, and tell users which
extra pip package to install in Copier's own env
(pipx inject copier <pkg> / uv tool install --with <pkg> copier).
- One template = one repo. Don't host multiple versioned templates in one
repo to share tags — subdirectory-per-variant keyed off an answer
(
_subdirectory: "{{ engine }}") is the supported exception.
copier update needs Git on both template and project sides for smart
merge; recopy is the fallback when that contract is broken.
9. Local verification loop (run before commit/release)
copier copy --trust --defaults --skip-tasks . /tmp/copier-test # fast render check
rm -rf /tmp/copier-test && copier copy --trust --defaults -r HEAD . /tmp/copier-test
cd /tmp/copier-test && git init && git add . && git commit -qm init
copier check-update # expect "up-to-date"
# simulate template change → tag → copier update --trust --defaults
Also run the project's own hygiene (mise run check / pre-commit run --all-files)
in the generated copy, not just in the template repo.
10. References
- Docs: https://copier.readthedocs.io/en/stable/ (creating, configuring,
generating, updating, settings, FAQ)
- Public templates:
https://github.com/topics/copier-template
- Local example in this workspace:
copier.yaml + template/ (subdirectory
pattern, _preserve, tasks, slug validator, answers-file template)
1---2name: copier3description: Expert guide for working with the Copier template ecosystem (copy, update, recopy, check-update, authoring copier.yaml/copier.yml, Jinja templating, questions, tasks, migrations, answers files). Use this skill whenever the user mentions copier, copier.yaml, copier.yml, copier template, scaffolding a project from a template, updating a generated project, .copier-answers.yml, recopy, Jinja suffix, _tasks, _migrations, _exclude, _subdirectory, or asks to create, debug, test, or update a copier template or generated project — even if they don't say the word "copier" explicitly but describe template-driven project generation or lifecycle updates.4---56# Copier — template authoring and project lifecycle78Copier renders project templates (Jinja + YAML questionnaire) and manages the9lifecycle of generated projects. Two audiences: **template authors** (create/maintain10templates) and **consumers** (copy/update projects).1112## 1. Core operations — use the right one1314| Command | Purpose | When to use |15|---|---|---|16| `copier copy <src> <dst>` | Generate new project | First render; also overlays onto preexisting dir |17| `copier update` (run inside project) | Smart update to newer template | Template evolved; preserves local edits via 3-way merge |18| `copier recopy` | Dumb re-render, keep answers, discard history | Broken update, deleted-file recovery, or update algorithm can't run |19| `copier check-update` | Report if template has newer version | Manual (`plain`) or CI (`--output-format json` / `--quiet` exit 2 = update available) |2021Common flags (copy):2223```bash24copier copy --trust <src> <dst> # required if template has _tasks/_migrations/_jinja_extensions25copier copy --trust --defaults <src> <dst> # non-interactive, all defaults26copier copy -d 'key=value' -d 'list=[a, b]' <src> <dst> # override answers27copier copy --data-file answers.yml <src> <dst> # bulk answers (--data wins on conflict)28copier copy --vcs-ref HEAD <src> <dst> # dev: include dirty/unreleased changes29copier copy --vcs-ref v2.0.0 <src> <dst> # pin version30copier copy --skip-tasks <src> <dst> # skip _tasks (NOT migrations)31copier copy --pretend <src> <dst> # dry run32copier copy --overwrite <src> <dst> # overwrite without asking33copier copy -f <src> <dst> # = --defaults --overwrite34```3536Update flags (run in project dir, clean `git status` first):3738```bash39copier update --trust # standard40copier update --trust --defaults # reuse all prior answers41copier update --trust --defaults -d 'q=new' # change one answer only42copier update --vcs-ref=:current: # re-answer questions, keep template version43copier update --conflict rej|inline # conflict style (default inline)44copier update --skip-answered # keep recorded answers, don't re-ask45```4647## 2. Template anatomy4849```text50my-template/ # usually a Git repo with PEP 440 tags (v1.0.0)51├── copier.yaml (or copier.yml) # questions + _settings (underscore-prefixed)52├── template/ # actual payload when _subdirectory: template53│ ├── {{ _copier_conf.answers_file }}.jinja54│ ├── README.md.jinja # *.jinja → rendered, suffix stripped55│ └── .gitignore # no suffix → copied verbatim56└── includes/ (optional) # macros/partials — must be _excluded57```5859Key settings in `copier.yaml`:6061```yaml62_min_copier_version: "9.0.0" # abort if installed copier is older63_subdirectory: template # isolate payload from template meta files64_templates_suffix: .jinja # which files Jinja renders ("" = render everything)65_answers_file: .copier-answers.yml66_preserve: [.copier-answers.yml]67_exclude: ["~*", "*.py[co]", __pycache__, "*.rej"]68_tasks: ["git init", "mise install"]69_message_after_copy: |70 Your project "{{ project_name }}" was created. Run `mise run check`.71_message_after_update: |72 Your project "{{ project_name }}" was updated. Resolve conflicts, then check.73```7475`_exclude` vs `_skip_if_exists` vs `_tasks`-only-once:7677- `_exclude`: never copy (gitignore syntax via `pathspec`; `!` negates).78 Templatable. Patterns match **destination paths** (after `.jinja` stripping),79 so `*.bar` already covers `foo.bar.jinja` → `foo.bar`; do NOT add `*.bar.jinja`.80 Use `_copier_operation == 'update'` guard for copy-once files.81- `_skip_if_exists`: copy once; never overwrite if present; recreate on82 `update` if missing (good for generated secrets).83- `_exclude` with update-guard: never re-render on update even if missing.8485## 3. Questions — best practices8687Order matters: questions are asked top-to-bottom; a default/validator/`when`88can only reference **earlier** answers.8990```yaml91project_name:92 type: str93 help: Human-readable project name94 default: my base project9596project_slug:97 type: str98 help: URL/filesystem-safe slug99 default: "{{ project_name|lower|replace(' ', '-')|replace('_', '-') }}"100 validator: "{% if not (project_slug | regex_search('^[a-z][a-z0-9-]+$')) %}Use lowercase, digits, dashes; start with a letter.{% endif %}"101102use_ci:103 type: bool104 help: Add CI workflow?105 default: true106107ci_provider:108 type: str109 choices:110 GitHub CI: github # key shown to user, VALUE stored in template111 GitLab CI: gitlab112 default: github # default must be the VALUE, not the key113 when: "{{ use_ci }}" # skip unless use_ci is true114115deploy_key:116 type: str117 secret: true # hidden prompt, excluded from answers file118 default: "{{ _external_data.secrets.deploy_key | default('changeme', true) }}"119 placeholder: "paste deploy key" # visual hint only, not a value120```121122Rules:123124- `type`: `str|int|float|bool|json|yaml|path` (`yaml` default). Keep choice125 values to one type; prefer `str` and convert in template code.126- Always give `help` and a sane `default` (omit default only to force input).127 `--defaults` fails on default-less questions unless `-d` supplies them.128- `validator`: Jinja that renders **empty = valid**, non-empty = error message.129- `when`: `false` (boolean) or templated string. Skipped questions are not130 stored, but their default is in render context. Use `when: false` for computed131 values; render `{{ UNSET }}` as default to leave the var undefined.132- `choices`: default must match value type. For multiselect bracket values quote133 explicitly: `default: '["[", "]"]'`, CLI: `-d 'brackets=["[", "]"]'`.134- `secret: true` **requires** a real default of the question's type; the value135 never lands in the answers file. `default: null` does NOT satisfy this —136 verified on Copier 9: `copy --defaults` crashes with137 `InvalidTypeError: Invalid answer "None" ... of type "str"`. Use a static138 fallback or `_external_data` (see §5).139- Conditional/dynamic choices: either `validator` per choice (visible but140 disabled with message) or templated `choices: |` block (hidden). When mixing141 both, wrap validator in `{% raw %}...{% endraw %}`.142- Templating is allowed **only inside string values**, only with143 already-answered variables. Interactive answers are never re-rendered.144- Computed, non-asked value: `default: "{{ earlier_var + 1 }}"` + `when: false`.145 To freeze it across updates (e.g. `copyright_year`), also dump it explicitly146 in the answers template (see §5).147- Prefer well-known user defaults names so `settings.yml` reuse works:148 `user_name`, `user_email`, `github_user`, `gitlab_user`.149150## 4. Jinja rendering rules151152- Rendered: files ending in `_templates_suffix` (default `.jinja`) — suffix is153 stripped on output. Everything else copied verbatim. If both `README.md` and154 `README.md.jinja` exist, the non-suffixed one is **ignored**.155- Directory names are templated but must **NOT** end with the suffix.156- File/dir names, `_exclude`/`_skip_if_exists` patterns, `_messages_*`,157 `_tasks`, `_migrations`, question `default/help/choices/validator/when` can158 all contain Jinja.159- Conditional file: `{% if use_precommit %}.pre-commit-config.yaml{% endif %}.jinja`160 — suffix stays **outside** the `{% if %}` or the file is not recognized.161 Use single quotes in path conditions (double quotes are illegal on Windows).162- Multi-pattern conditional exclude: one list item can render a whole163 newline-separated gitignore block.164- `{% yield item from list %}{{ item }}{% endyield %}` in a path loops to165 generate many files/dirs; loop vars are in scope inside generated files.166- Reuse snippets via `{% include 'partial.jinja' %}` or167 `{% from 'macros.jinja' import thing %}` (paths relative to template root).168 Put partials in `includes/` and `_exclude` it, or use `_subdirectory` so they169 are never copied. In path names use `pathjoin('includes','x.jinja')` (POSIX170 separator required).171- Builtins: all Jinja2 + `jinja2-ansible-filters` (`to_nice_yaml`,172 `to_nice_json`, `regex_search`, `ans_random|hash('sha512')` for secrets, ...).173- `_envops` default keeps trailing newlines. Set174 `_envops: {undefined: jinja2.StrictUndefined}` to fail fast on typos.175- Useful context: `_copier_answers` (safe, serializable, has `_commit`,176 `_src_path`), `_copier_conf` (has `.data`, `.dst_path`, `.src_path`,177 `.sep`, `.os`, `.answers_file` — WARNING `.data` may contain secrets),178 `_folder_name`, `_copier_python`, `_copier_phase` (prompt/tasks/migrate/render),179 `_copier_operation` (copy/update — tasks/exclude only), `_external_data`,180 `UNSET`.181- `_external_data`: `{namespace: relative/path.yml}` lazily parsed as YAML.182 Use for multi-template composition (read parent answers) or loading ignored183 secrets. Paths outside project root require `--trust`.184185## 5. Answers file — the update contract186187Template must ship `{{ _copier_conf.answers_file }}.jinja` (default name188`.copier-answers.yml`) with exactly:189190```jinja191# Changes here will be overwritten by Copier192{{ _copier_answers|to_nice_yaml -}}193```194195- Commit it in generated projects. Without it there is no smart update.196- **NEVER edit it by hand** — it makes Copier believe a different answer set197 produced the project and corrupts future diffs. Change answers via198 `copier update --defaults -d 'q=new'`, never via editor.199- Secrets (`secret: true`) are excluded automatically — that is why they need200 `_external_data` round-tripping if they must persist.201- Multi-template projects: each template gets its own file202 (`-a .copier-answers.main.yml`, `-a .copier-answers.ci.yml`, ...) and is203 updated independently.204205## 6. Tasks and migrations (unsafe — need `--trust`)206207```yaml208_tasks:209 - "git init"210 - "git rev-parse --verify HEAD >/dev/null 2>&1 || git commit --allow-empty -m 'Init commit'"211 - ["mise", "install"] # array form: no shell, no escaping bugs212 - command: ["{{ _copier_python }}", task.py]213 when: "{{ _copier_operation == 'copy' }}"214 - command: rm {{ name }}/README.md215 when: "{{ _copier_conf.os in ['linux', 'macos'] }}"216217_migrations:218 - version: v2.0.0 # run only when old < v2.0.0 <= new (PEP 440)219 command: rm -rf ./old-folder220 when: "{{ _stage == 'before' }}"221```222223- `_tasks` run after **every** copy and update. `_migrations` run only on224 update (optionally version-gated, `before`/`after` stage via `_stage`).225 `--skip-tasks` skips tasks but **not** migrations.226- Each item runs in its own subprocess with `$STAGE`, `$VERSION_FROM`,227 `$VERSION_TO`, `$VERSION_CURRENT` (+ PEP 440-normalized variants) in env.228 Answers file is reloaded after `before` migrations, so they can rewrite answers.229- Keep tasks idempotent, fast, and offline-safe where possible; prefer array230 form; gate OS-specific commands on `_copier_conf.os`.231- Any use of tasks/migrations/`_jinja_extensions` makes `copier` abort with232 exit 4 unless consumer passes `--trust`/`--UNSAFE` (or marks the source in233 `trust:` in `settings.yml`). Verified: without `--trust` Copier aborts234 **before rendering anything** — it does NOT render files and silently skip235 tasks. To render without running tasks: `--trust --skip-tasks`.236237## 7. Versioning, update safety, conflict recovery238239- Tag template releases with stable PEP 440 versions (`v1.0.0`). Default copy240 and `update` resolve to the **latest tag**, not the branch tip. Never move a241 released tag; use branches or explicit `--vcs-ref` for moving refs.242- `--vcs-ref HEAD` = current checkout **including dirty files** (needed for243 local template dev). Without it, dirty files are silently ignored because a244 tag is checked out instead (FAQ gotcha). `--vcs-ref=:current:` = re-ask245 without changing version.246- Before `update`: clean `git status`. Add merge-conflict guard hooks:247 `check-merge-conflict --assume-in-merge` for `inline`, forbid `*.rej` for248 `rej` style.249- How update works: regen old-tag template → diff vs current project → apply250 pre-migrations → render new-tag template → replay diff → post-migrations.251 Template-deleted-but-project-deleted paths stay deleted; `skip_if_exists`252 paths are always restored. If the old template can't be regenerated (missing253 external resource, incompatible Jinja extension, ancient Copier), fall back254 to `copier recopy` and resolve with git diff (loses smart merge for that run).255- Abort a bad update: `git reset; git checkout .; git clean -d -i`256 (`checkout <branch>` / `merge --abort` do NOT work).257258## 8. Caveats and known-issue checklist (verify before shipping)2592601. `_exclude` in YAML **replaces** defaults (you lose `copier.yaml`, `.git`, …).261 CLI `-x` **extends**. With `_subdirectory` set to a real dir, default262 `_exclude` becomes `[]`. `_exclude` matches **destination** paths.2632. `copier copy ./src ./dst` on a dirty template checks out latest **tag** —264 dirty files missing. Use `-r HEAD` while developing.2653. Shallow template clones cause huge git CPU use — use full clones.2664. Never put credentials in the source URL (`https://user:pass@…`) — they are267 recorded in `_src_path` in the answers file. Use SSH keys / credential helpers.2685. `secret` questions need a default; `choices` defaults must be values with269 matching `type`; multiselect bracket literals need inner quotes.2706. `when: false` values aren't stored — explicitly merge them into the answers271 dump if they must be frozen (`{{ dict(_copier_answers, foo=foo)|to_nice_yaml }}`).2727. Referencing a later question, or templating a key (not value), or unquoted273 `default: {{ 'x' }}` are all invalid — keep YAML valid, template values only.2748. `force` = skip prompts + overwrite; `defaults` = use defaults but still fail275 on default-less questions; `overwrite` = overwrite files only.276 `cleanup_on_error` deletes dst only if Copier created it.2779. `preserve_symlinks: false` (default) replaces links with target content.27810. Copier ≤5 used `.tmpl` suffix and `[[ ]]` delimiters. For cross-version279 templates pin `_min_copier_version` and, if needed, set `_envops` to the280 legacy delimiters. Copier 7+ ignores the legacy fallback.28111. `_jinja_extensions` code runs at render — audit it, and tell users which282 extra pip package to install in Copier's own env283 (`pipx inject copier <pkg>` / `uv tool install --with <pkg> copier`).28412. One template = one repo. Don't host multiple versioned templates in one285 repo to share tags — subdirectory-per-variant keyed off an answer286 (`_subdirectory: "{{ engine }}"`) is the supported exception.28713. `copier update` needs Git on **both** template and project sides for smart288 merge; `recopy` is the fallback when that contract is broken.289290## 9. Local verification loop (run before commit/release)291292```bash293copier copy --trust --defaults --skip-tasks . /tmp/copier-test # fast render check294rm -rf /tmp/copier-test && copier copy --trust --defaults -r HEAD . /tmp/copier-test295cd /tmp/copier-test && git init && git add . && git commit -qm init296copier check-update # expect "up-to-date"297# simulate template change → tag → copier update --trust --defaults298```299300Also run the project's own hygiene (`mise run check` / `pre-commit run --all-files`)301in the generated copy, not just in the template repo.302303## 10. References304305- Docs: https://copier.readthedocs.io/en/stable/ (creating, configuring,306 generating, updating, settings, FAQ)307- Public templates: `https://github.com/topics/copier-template`308- Local example in this workspace: `copier.yaml` + `template/` (subdirectory309 pattern, `_preserve`, tasks, slug validator, answers-file template)