docs/migration.md
Move one hook at a time when replacing an existing setup. Keep the old hook
available until the equivalent prek hook has run successfully on the whole
repository and in CI.
Start with the two-step path in the Quickstart. The Compatibility and Differences pages cover the cases worth checking when a repository depends on less common behavior.
A lint-staged command usually becomes a repo = "local" hook.
For example, this hook runs the project's ESLint installation and lets prek
append matching filenames:
=== "prek.toml"
```toml
[[repos]]
repo = "local"
[[repos.hooks]]
id = "eslint"
name = "eslint"
language = "system"
entry = "npm exec -- eslint"
files = "\\.[cm]?[jt]sx?$"
pass_filenames = true
```
=== ".pre-commit-config.yaml"
```yaml
repos:
- repo: local
hooks:
- id: eslint
name: eslint
language: system
entry: npm exec -- eslint
files: '\.[cm]?[jt]sx?$'
pass_filenames: true
```
The project must install its Node dependencies before this hook runs. Map the rest of the setup as follows:
| Existing concept | prek equivalent |
|---|---|
| lint-staged file glob | files, types, types_or, and exclude |
| Filenames appended to a command | pass_filenames = true, which is the default |
| Command discovers its own files | pass_filenames = false |
| Shell pipeline or expansion | Prefer a direct command; otherwise set shell explicitly |
| Husky hook script | A Git stage plus prek install |
If a Husky script also performs unrelated work, keep that work in a project script and call the script from a local hook. This keeps the Git shim small and makes the command easy to run outside Git.
Translate each Lefthook command into a local hook:
| Lefthook concept | prek equivalent |
|---|---|
Hook name such as pre-commit or pre-push | Hook stages and an installed Git shim |
commands.<name>.run | Local hook entry and args |
{staged_files} | The default pass_filenames = true behavior |
glob and exclude | files, types, and exclude |
| Parallel command groups | Hooks with the same priority |
Installing a shim and making a hook eligible for that stage are separate choices. For example:
default_install_hook_types = ["pre-commit", "pre-push"]
[[repos]]
repo = "local"
[[repos.hooks]]
id = "tests"
name = "tests"
language = "system"
entry = "cargo test"
pass_filenames = false
stages = ["pre-push"]
Hooks remain sequential when priority is omitted. Give independent hooks the
same explicit priority only after
checking that they do not modify the same files or contend for shared state.
If .git/hooks/<hook-name> already belongs to another tool, a normal
prek install moves it to <hook-name>.legacy and installs prek in migration
mode. The prek shim runs both hook implementations.
prek install
When the migration is complete, replace the legacy hook:
prek install --force
Before using --force, make sure the old hook contains no checks that are still
needed. If you uninstall while migration mode is active, prek uninstall
restores the legacy hook to its original path.
prek run --all-files.