Skip to content

Node hook install corrupts the user's git index when committing from a linked worktree #2505

Description

@will-wright-eng

Before submitting

  • I have searched the issue list for similar bug reports.

Summary

Running prek as the pre-commit hook during git commit in a linked git worktree (git worktree add) can replace the worktree's git index with the file tree of a cached hook repository. After the run, every index-reading git command fails:

fatal: unable to read d3e222c562aeccbbd7e19d83e81daf6c54c4410d

The staged blobs only exist in the hook repo's object database, not the user repo's. All hooks report "Passed" — the corruption is only discovered on the next index read (a later hook, or git status after the commit fails with error: Error building trees).

Root cause chain:

  1. git commit exports GIT_INDEX_FILE to the hook. From a linked worktree these are absolute paths (<main>/.git/worktrees/<name>/index, plus absolute GIT_DIR); from a main worktree GIT_INDEX_FILE is relative (.git/index), which is why only linked worktrees are affected.
  2. prek keeps these variables in its environment (its own git commands need them) and passes them to every subprocess.
  3. Since Install Node hooks through git url #2394 (v0.4.11), node hook environments are installed by giving npm the cached clone as a git+file:// URL. npm's pacote GitFetcher then shells out to git with the inherited environment.
  4. git clone honors an inherited GIT_INDEX_FILE: its initial checkout writes the user's index, filling it with the hook repo's tree.

The errant command, captured with GIT_TRACE during the repro below:

trace: built-in: git clone file:///…/prek/repos/29c5ebd8dba238ce /…/prek/cache/npm/_cacache/tmp/git-cloneXXXX --recurse-submodules

Triggering requires a cold node env (first install, or env invalidated by a prek upgrade). A warm repo cache does not protect — npm clones from it. Python pre-commit strips exactly these variables for its internal git operations (no_git_env() in pre_commit/git.py); prek's own clone commands already do the same via isolate_from_git_env(), but the variables still leak through non-git subprocesses like npm.

Minimal repro (no real commit needed — simulates the env git exports to the hook):

tmp=$(mktemp -d) && cd "$tmp"
git init repro && cd repro
git config user.email t@t
git config user.name t

cat > .pre-commit-config.yaml <<'EOF'
repos:
  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.45.0
    hooks:
      - id: markdownlint
EOF
printf '# Doc\n' > README.md
git add . && git commit -m init
echo change > staged.txt && git add .

git ls-files --stage   # snapshot: 3 entries

# Simulate the environment `git commit` exports to the
# pre-commit hook when committing from a linked worktree
# (absolute GIT_DIR / GIT_INDEX_FILE):
gitdir=$(git rev-parse --absolute-git-dir)
export GIT_DIR="$gitdir"
export GIT_INDEX_FILE="$gitdir/index"
export PREK_HOME="$tmp/prek-home"

prek run --all-files

git ls-files --stage   # ACTUAL: the hook repo's ~90 files
git status             # fatal: unable to read d3e222c5...
  • Expected: the index is unchanged by prek run.
  • Actual: the index contains markdownlint-cli's tree; the repo is unusable until the index is rebuilt.

The same corruption reproduces end-to-end with prek installed as the git hook, a workspace-mode monorepo, and git add . && git commit in a fresh linked worktree.

Recovery for affected repos:

rm "$(git rev-parse --absolute-git-dir)/index" && git reset

The mechanism is also demonstrable without prek — git clone alone clobbers a foreign index through the env var:

GIT_INDEX_FILE=/path/to/victim/.git/index git clone <any-repo> /tmp/x

Platform

macOS 26.5 arm64 (Darwin 25.5.0); git 2.50.1 (Apple Git-155)

Version (current: 0.4.12)

prek 0.4.12 (Homebrew, built 2026-08-03) — affects ≥ 0.4.11 (git+file:// node install, #2394)

.pre-commit-config.yaml

repos:
  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.45.0
    hooks:
      - id: markdownlint

Log file

prek's own log shows a normal, fully successful run — the index write happens inside
npm's git subprocesses during hook env installation and never surfaces in prek's log.
The errant command is visible by running the repro with GIT_TRACE=<file>:

trace: built-in: git clone file:///…/prek/repos/29c5ebd8dba238ce /…/prek/cache/npm/_cacache/tmp/git-cloneXXXX --recurse-submodules

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions